Showcase Carousel

A showcase carousel component for featured content.

Preview

Code

Code

app/page.tsx

1"use client" 2 3import ShowcaseCard from "@/components/showcase-card"; 4import { 5 Carousel, 6 CarouselContent, 7 CarouselItem, 8 CarouselNext, 9 CarouselPrevious 10} from "@/components/ui/carousel"; 11import Autoplay from "embla-carousel-autoplay"; 12 13const ShowcasePage = () => { 14 return ( 15 <section className=" w-full py-20 flex items-center justify-center bg-black h-screen"> 16 <div className=" w-full relative group"> 17 <div className="absolute top-0 bottom-0 left-0 w-[2%] md:w-[20%] z-[1] bg-gradient-to-r from-black to-transparent" /> 18 <div className="absolute top-0 bottom-0 right-0 w-[2%] md:w-[20%] z-[1] bg-gradient-to-l from-black to-transparent" /> 19 20 <Carousel 21 plugins={[ 22 Autoplay({ 23 delay: 5000, 24 }), 25 ]} 26 opts={{ 27 align: "center", 28 loop: true, 29 }} 30 className="w-full" 31 > 32 <CarouselContent className="px-3 md:px-0"> 33 <CarouselItem className=" basis-[90%] md:basis-1/4"> 34 <ShowcaseCard color="#FB3F00" icon="/assets/showcase-icon-1.png" image="/assets/showcase-1.png" video="/videos/showcase-1.mp4" title="Lovi" link="www.lovi.com" /> 35 </CarouselItem> 36 <CarouselItem className=" basis-[90%] md:basis-1/4"> 37 <ShowcaseCard color="#1500FF" icon="/assets/showcase-icon-2.png" image="/assets/showcase-2.png" video="/videos/showcase-2.mp4" title="Lovi" link="www.lovi.com" /> 38 </CarouselItem> 39 <CarouselItem className=" basis-[90%] md:basis-1/4"> 40 <ShowcaseCard color="#85C25D" icon="/assets/showcase-icon-3.png" image="/assets/showcase-3.png" video="/videos/showcase-3.mp4" title="Lovi" link="www.lovi.com" /> 41 </CarouselItem> 42 <CarouselItem className=" basis-[90%] md:basis-1/4"> 43 <ShowcaseCard color="#FF8E1C" icon="/assets/showcase-icon-2.png" image="/assets/showcase-4.png" title="Lovi" link="www.lovi.com" /> 44 </CarouselItem> 45 <CarouselItem className=" basis-[90%] md:basis-1/4"> 46 <ShowcaseCard color="#4A4A2F" icon="/assets/showcase-icon-1.png" image="/assets/showcase-5.png" title="Lovi" link="www.lovi.com" /> 47 </CarouselItem> 48 </CarouselContent> 49 <div className=" opacity-0 group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity duration-200 ease-in pointer-events-none"> 50 <CarouselPrevious /> 51 52 <CarouselNext /> 53 </div> 54 </Carousel> 55 </div> 56 </section> 57 ); 58} 59 60export default ShowcasePage;

Installation

Initialized a project and installed the necessary dependencies to use this component.

1. Initialize a new Next.js project:

Code
npx create-next-app@latest my-app

2. Install Shadcn UI

Code
npx shadcn@latest init

3. Install Shadcn Components

Code
npx shadcn@latest add carousel button

4. Install Embla Carousel Autoplay

Code
npm install embla-carousel-autoplay --save

Add Showcase Card Component

Code

src/components/showcase-card.tsx

1import Image from "next/image"; 2import Link from "next/link"; 3 4const ShowcaseCard = ({ 5 title, 6 image, 7 link, 8 video, 9 icon, 10 color 11}: { 12 title: string; 13 video?: string; 14 image: string; 15 link: string; 16 icon: string; 17 color: string; 18}) => { 19 return ( 20 <div className=" w-full group/card"> 21 {video ? ( 22 <div style={{ backgroundColor: color }} className=" rounded-xl w-full p-2.5 md:p-4 relative"> 23 <Link rel="" target="_blank" href={link}> 24 <div className="absolute hidden rounded-lg group-hover/card:block inset-0 z-20 hover:block cursor-pointer opacity-40" style={{ backgroundColor: color }} /> 25 </Link> 26 <video 27 className="w-full h-auto aspect-square rounded-lg" 28 playsInline 29 autoPlay 30 loop 31 muted 32 poster={image} 33 > 34 <source src={video} type="video/mp4" /> 35 Your browser does not support the video tag. 36 </video> 37 </div> 38 ) : ( 39 <div className=" w-full relative h-auto aspect-square"> 40 <Link rel="" target="_blank" href={link}> 41 <div className="absolute hidden rounded-lg group-hover/card:block inset-0 z-20 hover:block cursor-pointer opacity-40" style={{ backgroundColor: color }} /> 42 </Link> 43 <Image 44 src={image} 45 alt={title} 46 fill 47 className="object-cover rounded-lg" 48 /> 49 </div> 50 )} 51 <div className="flex items-center gap-4 mt-4"> 52 <Image width={40} height={40} src={icon} className=" object-cover border rounded-full" alt={title} /> 53 <div className="flex flex-col items-start text-white"> 54 <p>{link}</p> 55 <p>{title}</p> 56 </div> 57 </div> 58 </div> 59 ); 60} 61 62export default ShowcaseCard;

Update Carousel Component

Code

src/components/ui/carousel.tsx

1"use client" 2 3import useEmblaCarousel, { 4 type UseEmblaCarouselType, 5} from "embla-carousel-react" 6import { ChevronLeft, ChevronRight } from "lucide-react" 7import * as React from "react" 8 9import { Button } from "@/components/ui/button" 10import { cn } from "@/lib/utils" 11 12type CarouselApi = UseEmblaCarouselType[1] 13type UseCarouselParameters = Parameters<typeof useEmblaCarousel> 14type CarouselOptions = UseCarouselParameters[0] 15type CarouselPlugin = UseCarouselParameters[1] 16 17type CarouselProps = { 18 opts?: CarouselOptions 19 plugins?: CarouselPlugin 20 orientation?: "horizontal" | "vertical" 21 setApi?: (api: CarouselApi) => void 22} 23 24type CarouselContextProps = { 25 carouselRef: ReturnType<typeof useEmblaCarousel>[0] 26 api: ReturnType<typeof useEmblaCarousel>[1] 27 scrollPrev: () => void 28 scrollNext: () => void 29 canScrollPrev: boolean 30 canScrollNext: boolean 31} & CarouselProps 32 33const CarouselContext = React.createContext<CarouselContextProps | null>(null) 34 35function useCarousel() { 36 const context = React.useContext(CarouselContext) 37 38 if (!context) { 39 throw new Error("useCarousel must be used within a <Carousel />") 40 } 41 42 return context 43} 44 45function Carousel({ 46 orientation = "horizontal", 47 opts, 48 setApi, 49 plugins, 50 className, 51 children, 52 ...props 53}: React.ComponentProps<"div"> & CarouselProps) { 54 const [carouselRef, api] = useEmblaCarousel( 55 { 56 ...opts, 57 axis: orientation === "horizontal" ? "x" : "y", 58 }, 59 plugins 60 ) 61 const [canScrollPrev, setCanScrollPrev] = React.useState(false) 62 const [canScrollNext, setCanScrollNext] = React.useState(false) 63 64 const onSelect = React.useCallback((api: CarouselApi) => { 65 if (!api) return 66 setCanScrollPrev(api.canScrollPrev()) 67 setCanScrollNext(api.canScrollNext()) 68 }, []) 69 70 const scrollPrev = React.useCallback(() => { 71 api?.scrollPrev() 72 }, [api]) 73 74 const scrollNext = React.useCallback(() => { 75 api?.scrollNext() 76 }, [api]) 77 78 const handleKeyDown = React.useCallback( 79 (event: React.KeyboardEvent<HTMLDivElement>) => { 80 if (event.key === "ArrowLeft") { 81 event.preventDefault() 82 scrollPrev() 83 } else if (event.key === "ArrowRight") { 84 event.preventDefault() 85 scrollNext() 86 } 87 }, 88 [scrollPrev, scrollNext] 89 ) 90 91 React.useEffect(() => { 92 if (!api || !setApi) return 93 setApi(api) 94 }, [api, setApi]) 95 96 React.useEffect(() => { 97 if (!api) return 98 onSelect(api) 99 api.on("reInit", onSelect) 100 api.on("select", onSelect) 101 102 return () => { 103 api?.off("select", onSelect) 104 } 105 }, [api, onSelect]) 106 107 return ( 108 <CarouselContext.Provider 109 value={{ 110 carouselRef, 111 api: api, 112 opts, 113 orientation: 114 orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), 115 scrollPrev, 116 scrollNext, 117 canScrollPrev, 118 canScrollNext, 119 }} 120 > 121 <div 122 onKeyDownCapture={handleKeyDown} 123 className={cn("relative", className)} 124 role="region" 125 aria-roledescription="carousel" 126 data-slot="carousel" 127 {...props} 128 > 129 {children} 130 </div> 131 </CarouselContext.Provider> 132 ) 133} 134 135function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { 136 const { carouselRef, orientation } = useCarousel() 137 138 return ( 139 <div 140 ref={carouselRef} 141 className="overflow-hidden" 142 data-slot="carousel-content" 143 > 144 <div 145 className={cn( 146 "flex", 147 orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", 148 className 149 )} 150 {...props} 151 /> 152 </div> 153 ) 154} 155 156function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { 157 const { orientation } = useCarousel() 158 159 return ( 160 <div 161 role="group" 162 aria-roledescription="slide" 163 data-slot="carousel-item" 164 className={cn( 165 "min-w-0 shrink-0 grow-0 basis-full", 166 orientation === "horizontal" ? "pl-4" : "pt-4", 167 className 168 )} 169 {...props} 170 /> 171 ) 172} 173 174function CarouselPrevious({ 175 className, 176 variant = "outline", 177 size = "icon", 178 ...props 179}: React.ComponentProps<typeof Button>) { 180 const { orientation, scrollPrev, canScrollPrev } = useCarousel() 181 182 return ( 183 <Button 184 data-slot="carousel-previous" 185 variant={variant} 186 size={size} 187 className={cn( 188 "absolute h-8 md:h-9 w-8 md:w-9 rounded-full hover:bg-white hover:text-black bg-black/50 backdrop-blur-md text-white border-none z-50", 189 orientation === "horizontal" 190 ? " left-12 md:left-[28rem] top-1/2 -translate-y-[100%]" 191 : "-top-12 left-1/2 -translate-x-1/2 rotate-90", 192 className 193 )} 194 disabled={!canScrollPrev} 195 onClick={scrollPrev} 196 {...props} 197 > 198 <ChevronLeft className=" size-5" /> 199 <span className="sr-only">Previous slide</span> 200 </Button> 201 ) 202} 203 204function CarouselNext({ 205 className, 206 variant = "outline", 207 size = "icon", 208 ...props 209}: React.ComponentProps<typeof Button>) { 210 const { orientation, scrollNext, canScrollNext } = useCarousel() 211 212 return ( 213 <Button 214 data-slot="carousel-next" 215 variant={variant} 216 size={size} 217 className={cn( 218 "absolute h-8 md:h-9 w-8 md:w-9 rounded-full hover:bg-white bg-black/50 hover:text-black backdrop-blur-md text-white border-none z-50", 219 orientation === "horizontal" 220 ? " right-12 md:right-[28rem] top-1/2 -translate-y-[100%]" 221 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", 222 className 223 )} 224 disabled={!canScrollNext} 225 onClick={scrollNext} 226 {...props} 227 > 228 <ChevronRight className=" size-5" /> 229 <span className="sr-only">Next slide</span> 230 </Button> 231 ) 232} 233 234export { 235 Carousel, 236 CarouselContent, 237 CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi 238}

Add Button Component

Code

src/components/ui/button.tsx

1import * as React from "react" 2import { Slot } from "@radix-ui/react-slot" 3import { cva, type VariantProps } from "class-variance-authority" 4 5import { cn } from "@/lib/utils" 6 7const buttonVariants = cva( 8 "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 { 10 variants: { 11 variant: { 12 default: 13 "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 destructive: 15 "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 outline: 17 "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 secondary: 19 "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 ghost: 21 "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 link: "text-primary underline-offset-4 hover:underline", 23 }, 24 size: { 25 default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 icon: "size-9", 29 }, 30 }, 31 defaultVariants: { 32 variant: "default", 33 size: "default", 34 }, 35 } 36) 37 38function Button({ 39 className, 40 variant, 41 size, 42 asChild = false, 43 ...props 44}: React.ComponentProps<"button"> & 45 VariantProps<typeof buttonVariants> & { 46 asChild?: boolean 47 }) { 48 const Comp = asChild ? Slot : "button" 49 50 return ( 51 <Comp 52 data-slot="button" 53 className={cn(buttonVariants({ variant, size, className }))} 54 {...props} 55 /> 56 ) 57} 58 59export { Button, buttonVariants }

Add Global Styles

Code

src/app/global.css

1@import "tailwindcss"; 2@import "tw-animate-css"; 3 4@custom-variant dark (&:is(.dark *)); 5 6@theme inline { 7 --color-background: var(--background); 8 --color-foreground: var(--foreground); 9 --font-inter: var(--font-inter); 10 --color-sidebar-ring: var(--sidebar-ring); 11 --color-sidebar-border: var(--sidebar-border); 12 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 13 --color-sidebar-accent: var(--sidebar-accent); 14 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 15 --color-sidebar-primary: var(--sidebar-primary); 16 --color-sidebar-foreground: var(--sidebar-foreground); 17 --color-sidebar: var(--sidebar); 18 --color-chart-5: var(--chart-5); 19 --color-chart-4: var(--chart-4); 20 --color-chart-3: var(--chart-3); 21 --color-chart-2: var(--chart-2); 22 --color-chart-1: var(--chart-1); 23 --color-ring: var(--ring); 24 --color-input: var(--input); 25 --color-border: var(--border); 26 --color-destructive: var(--destructive); 27 --color-accent-foreground: var(--accent-foreground); 28 --color-accent: var(--accent); 29 --color-muted-foreground: var(--muted-foreground); 30 --color-muted: var(--muted); 31 --color-secondary-foreground: var(--secondary-foreground); 32 --color-secondary: var(--secondary); 33 --color-primary-foreground: var(--primary-foreground); 34 --color-primary: var(--primary); 35 --color-popover-foreground: var(--popover-foreground); 36 --color-popover: var(--popover); 37 --color-card-foreground: var(--card-foreground); 38 --color-card: var(--card); 39 --radius-sm: calc(var(--radius) - 4px); 40 --radius-md: calc(var(--radius) - 2px); 41 --radius-lg: var(--radius); 42 --radius-xl: calc(var(--radius) + 4px); 43} 44 45:root { 46 --radius: 0.625rem; 47 --background: oklch(1 0 0); 48 --foreground: oklch(0.145 0 0); 49 --card: oklch(1 0 0); 50 --card-foreground: oklch(0.145 0 0); 51 --popover: oklch(1 0 0); 52 --popover-foreground: oklch(0.145 0 0); 53 --primary: oklch(0.205 0 0); 54 --primary-foreground: oklch(0.985 0 0); 55 --secondary: oklch(0.97 0 0); 56 --secondary-foreground: oklch(0.205 0 0); 57 --muted: oklch(0.97 0 0); 58 --muted-foreground: oklch(0.556 0 0); 59 --accent: oklch(0.97 0 0); 60 --accent-foreground: oklch(0.205 0 0); 61 --destructive: oklch(0.577 0.245 27.325); 62 --border: oklch(0.922 0 0); 63 --input: oklch(0.922 0 0); 64 --ring: oklch(0.708 0 0); 65 --chart-1: oklch(0.646 0.222 41.116); 66 --chart-2: oklch(0.6 0.118 184.704); 67 --chart-3: oklch(0.398 0.07 227.392); 68 --chart-4: oklch(0.828 0.189 84.429); 69 --chart-5: oklch(0.769 0.188 70.08); 70 --sidebar: oklch(0.985 0 0); 71 --sidebar-foreground: oklch(0.145 0 0); 72 --sidebar-primary: oklch(0.205 0 0); 73 --sidebar-primary-foreground: oklch(0.985 0 0); 74 --sidebar-accent: oklch(0.97 0 0); 75 --sidebar-accent-foreground: oklch(0.205 0 0); 76 --sidebar-border: oklch(0.922 0 0); 77 --sidebar-ring: oklch(0.708 0 0); 78} 79 80.dark { 81 --background: oklch(0.145 0 0); 82 --foreground: oklch(0.985 0 0); 83 --card: oklch(0.205 0 0); 84 --card-foreground: oklch(0.985 0 0); 85 --popover: oklch(0.205 0 0); 86 --popover-foreground: oklch(0.985 0 0); 87 --primary: oklch(0.922 0 0); 88 --primary-foreground: oklch(0.205 0 0); 89 --secondary: oklch(0.269 0 0); 90 --secondary-foreground: oklch(0.985 0 0); 91 --muted: oklch(0.269 0 0); 92 --muted-foreground: oklch(0.708 0 0); 93 --accent: oklch(0.269 0 0); 94 --accent-foreground: oklch(0.985 0 0); 95 --destructive: oklch(0.704 0.191 22.216); 96 --border: oklch(1 0 0 / 10%); 97 --input: oklch(1 0 0 / 15%); 98 --ring: oklch(0.556 0 0); 99 --chart-1: oklch(0.488 0.243 264.376); 100 --chart-2: oklch(0.696 0.17 162.48); 101 --chart-3: oklch(0.769 0.188 70.08); 102 --chart-4: oklch(0.627 0.265 303.9); 103 --chart-5: oklch(0.645 0.246 16.439); 104 --sidebar: oklch(0.205 0 0); 105 --sidebar-foreground: oklch(0.985 0 0); 106 --sidebar-primary: oklch(0.488 0.243 264.376); 107 --sidebar-primary-foreground: oklch(0.985 0 0); 108 --sidebar-accent: oklch(0.269 0 0); 109 --sidebar-accent-foreground: oklch(0.985 0 0); 110 --sidebar-border: oklch(1 0 0 / 10%); 111 --sidebar-ring: oklch(0.556 0 0); 112} 113 114@layer base { 115 * { 116 @apply border-border outline-ring/50; 117 } 118 body { 119 @apply bg-background text-foreground; 120 } 121}

Become an Astrae
Affiliate Today

Make referrals, and bring in clients. Keep 50% of your earnings paid out weekly.

Description