# Card carousel (themes.dev UI block)

A row of image cards with a title, a line and a link that scrolls sideways; Previous and Next turn off at the ends. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

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

## 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`
- HTML: https://www.themes.dev/ui/r/html/card-carousel.html (with https://www.themes.dev/ui/r/html/caseroom.css)

Page: https://www.themes.dev/ui/blocks/card-carousel/ · Rules for agents: https://www.themes.dev/ui/AGENT.md

## Code

### card-carousel.astro

```astro
---
import Carousel from '@/components/ui/carousel.astro';
import Link from '@/components/ui/link.astro';
import { cn } from '@/lib/utils';
import { cardCarousel as c, type CardCarouselContent } from './card-carousel.classes';

type Props = CardCarouselContent & { class?: string };
const { title, id, cards, class: className } = Astro.props;
---

<div class={cn(c.root, className)}>
  <div class={c.inner}>
<Carousel id={id} title={title}>
  {
    cards.map((card) => (
      <li class={c.slide}>
        <article class={c.card}>
          <div class={c.frame}>
            <img src={card.image.src} alt={card.image.alt} width="640" height="480" class={c.image} loading="lazy" decoding="async" />
          </div>
          <div class={c.body}>
            <h3 class={c.title}>{card.title}</h3>
            {card.text ? <p class={c.text}>{card.text}</p> : null}
            <Link href={card.href} variant="standalone" class={c.link}>{card.linkLabel}</Link>
          </div>
        </article>
      </li>
    ))
  }
</Carousel>
  </div>
</div>
```

### card-carousel.classes.ts

```tsx
/** 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

```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>
  );
}
```
