# Carousel 03

> A carousel of cards with a title and description.

A shadcn/ui block from the [@flx](https://ui.flexnative.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/flx/carousel-03).

- Page, with a live preview: https://shadcn.exchange/r/flx/carousel-03
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/flx/carousel-03-light-bca55bb0ccc4ffa4.png
- Registry item: https://ui.flexnative.com/r/carousel-03.json
- Listed: 2026-10-11, source last changed 2026-10-11

## Install

```bash
npx shadcn@latest add https://ui.flexnative.com/r/carousel-03.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `carousel`, `button`

## Files

### `registry/blocks/carousel/carousel-03/carousel-03.tsx`

```tsx
'use client'

import { ChevronLeft, ChevronRight } from 'lucide-react'
import * as React from 'react'

import { Button } from '@/components/ui/button'
import {
  Carousel,
  type CarouselApi,
  CarouselContent,
  CarouselItem,
} from '@/components/ui/carousel'
import { cn } from '@/lib/utils'

export type CarouselCardItem = {
  title?: string
  description?: string
  media: {
    src: string
    alt?: string
  }
}

export type Carousel03Props = {
  title?: string
  description?: string
  items: CarouselCardItem[]
  className?: string
}

export function Carousel03({
  title,
  description,
  items,
  className,
}: Carousel03Props) {
  const [api, setApi] = React.useState<CarouselApi>()
  const [canPrev, setCanPrev] = React.useState(false)
  const [canNext, setCanNext] = React.useState(false)

  React.useEffect(() => {
    if (!api) return

    const onSelect = () => {
      setCanPrev(api.canScrollPrev())
      setCanNext(api.canScrollNext())
    }

    api.on('select', onSelect)
    api.on('reInit', onSelect)
    setTimeout(() => {
      onSelect()
    }, 0)

    return () => {
      api.off('select', onSelect)
      api.off('reInit', onSelect)
    }
  }, [api])

  const scrollPrev = React.useCallback(() => api?.scrollPrev(), [api])
  const scrollNext = React.useCallback(() => api?.scrollNext(), [api])

  if (!items || items.length === 0) return null

  const containerWidthClassName = cn('w-full max-w-6xl mx-auto px-4', className)

  return (
    <div className="flex w-full flex-col gap-8 md:gap-10">
      <div
        className={cn(
          'flex flex-wrap items-start justify-between gap-2',
          containerWidthClassName,
        )}
      >
        <h2 className="min-w-[200px] flex-1 text-2xl font-semibold sm:max-w-md md:text-3xl lg:max-w-lg">
          {title}
        </h2>

        <p className="text-muted-foreground w-full text-sm sm:w-auto sm:max-w-md md:text-base lg:max-w-lg">
          {description}
        </p>
      </div>

      <Carousel
        setApi={setApi}
        opts={{
          align: 'start',
        }}
        className="w-full"
        aria-label="Carousel 03"
      >
        <CarouselContent
          className={cn(
            'h-auto select-none first:!pl-0',
            containerWidthClassName,
          )}
        >
          {items.map((item, i) => (
            <CarouselItem
              key={`${item.title}-${i}`}
              className="basis-[80%] pl-4 md:basis-[45%] lg:basis-[36%]"
            >
              <div className="relative w-full overflow-hidden rounded-lg">
                <img
                  src={item.media.src}
                  alt={item.media.alt ?? item.title}
                  loading="lazy"
                  decoding="async"
                  className="h-[220px] w-full object-cover md:h-[300px] lg:h-[340px]"
                />
              </div>
              <div className="mt-3">
                {item.title && (
                  <h3 className="text-lg font-medium">{item.title}</h3>
                )}
                {item.description && (
                  <p className="text-muted-foreground text-sm">
                    {item.description}
                  </p>
                )}
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
      </Carousel>

      <div className={cn('flex justify-end gap-3', containerWidthClassName)}>
        <Button
          variant="outline"
          size="icon"
          onClick={scrollPrev}
          disabled={!canPrev}
          className="bg-muted/60 hover:bg-muted size-10 rounded-full border-none shadow-none"
        >
          <ChevronLeft />
          <span className="sr-only">Previous slide</span>
        </Button>
        <Button
          variant="outline"
          size="icon"
          onClick={scrollNext}
          disabled={!canNext}
          className="bg-muted/60 hover:bg-muted size-10 rounded-full border-none shadow-none"
        >
          <ChevronRight />
          <span className="sr-only">Next slide</span>
        </Button>
      </div>
    </div>
  )
}
```

### `registry/blocks/carousel/carousel-03/carousel-03-example.tsx`

```tsx
import { Carousel03, Carousel03Props } from './carousel-03'

export const values = {
  title: 'New blocks for modern UI building.',
  description: 'Fresh, reusable pieces for fast product work.',
  items: [
    {
      title: 'Beautiful blocks',
      description: 'Polished components that feel ready.',
      media: {
        src: 'https://images.unsplash.com/photo-1545277048-000c86055339?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
    {
      title: 'Ship faster',
      description: 'Move from idea to screen quickly.',
      media: {
        src: 'https://images.unsplash.com/photo-1574557398955-09174289ea44?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
    {
      title: 'Build clean',
      description: 'Simple structure with room to breathe.',
      media: {
        src: 'https://images.unsplash.com/photo-1595330495302-e257debfa664?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
    {
      title: 'Launch ready',
      description: 'Fast to use and easy to ship.',
      media: {
        src: 'https://images.unsplash.com/photo-1543617934-70e4a9efad1c?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
    {
      title: 'Made to scale',
      description: 'Reusable blocks for growing products.',
      media: {
        src: 'https://images.unsplash.com/photo-1574557399375-c721d2371825?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
    {
      title: 'Ready to use',
      description: 'Drop in and keep moving.',
      media: {
        src: 'https://images.unsplash.com/photo-1574557399909-c6df4fde191a?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
      },
    },
  ],
} satisfies Carousel03Props

export function Carousel03Example() {
  return (
    <Carousel03
      title={values.title}
      description={values.description}
      items={values.items}
    />
  )
}
```
