Skip to content
    Style: Wireframe

    Blocks · Display

    Card carousel

    A row of image cards with a title, a line and a link that scrolls sideways; Previous and Next turn off at the ends.

    22% of the 2,073 sites in The UI Index have a carousel (463 sites).

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/card-carousel.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/card-carousel.json

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    card-carousel.classes.ts
    /** A card carousel: a row of image cards, each with a title, a line and a link, that scrolls sideways. The row,
     *  the Previous and Next buttons (they turn off at the ends) and the keyboard and swipe behaviour are the carousel
     *  item's, so this block only dresses the slides. The picture is cropped to 4:3, so keep the subject in the middle.
     *  Titles stay on one or two lines; the links line up at the bottom whatever the text's length. */
    export const cardCarousel = {
      root: 'bg-background px-6 py-12 text-foreground',
      inner: 'mx-auto max-w-6xl',
      // The carousel item's slide width: one card and a bit of the next on a phone, more as the row widens.
      slide: 'w-[min(18rem,80%)] shrink-0 snap-start',
      card: 'flex h-full flex-col gap-3',
      frame: 'aspect-[4/3] overflow-hidden rounded-xl border bg-muted',
      image: 'size-full object-cover object-left-top dark:brightness-[.85]',
      body: 'flex flex-1 flex-col items-start gap-2',
      title: 'line-clamp-2 text-base font-semibold text-balance',
      text: 'text-sm text-pretty text-muted-foreground',
      link: 'mt-auto w-fit self-start pt-1.5',
    };
    
    export type CardCarouselCard = {
      /** One line, under about 28 characters. */
      title: string;
      /** At most 100 characters. */
      text?: string;
      /** The picture. `alt` says what it shows; use "" only when the title already says it. */
      image: { src: string; alt: string };
      href: string;
      /** The link under the text, at most 22 characters; make it differ from the other cards' ("Read the tabs guide"). */
      linkLabel: string;
    };
    
    export type CardCarouselContent = {
      /** The heading over the row, also the region's name. */
      title: string;
      id: string;
      cards: CardCarouselCard[];
    };
    card-carousel.tsx
    import { Carousel } from '@/components/ui/carousel';
    import { Link } from '@/components/ui/link';
    import { cn } from '@/lib/utils';
    import { cardCarousel as c, type CardCarouselContent } from './card-carousel.classes';
    
    export type CardCarouselProps = CardCarouselContent & { className?: string };
    
    /** A row of image cards with a title, a line and a link, that scrolls sideways; Previous and Next turn off at the ends. */
    export function CardCarousel({ title, id, cards, className }: CardCarouselProps) {
      return (
        <div className={cn(c.root, className)}>
          <div className={c.inner}>
        <Carousel id={id} title={title}>
          {cards.map((card) => (
            <li key={card.title} className={c.slide}>
              <article className={c.card}>
                <div className={c.frame}>
                  <img src={card.image.src} alt={card.image.alt} width={640} height={480} className={c.image} loading="lazy" decoding="async" />
                </div>
                <div className={c.body}>
                  <h3 className={c.title}>{card.title}</h3>
                  {card.text ? <p className={c.text}>{card.text}</p> : null}
                  <Link href={card.href} variant="standalone" className={c.link}>{card.linkLabel}</Link>
                </div>
              </article>
            </li>
          ))}
        </Carousel>
          </div>
        </div>
      );
    }