# Carousel 02

> A carousel of media 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-02).

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

## Install

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

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

## Dependencies

- npm packages: `lucide-react`, `react-wrap-balancer`
- Registry items: `carousel`, `button`

## Files

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

```tsx
'use client'

import type { EmblaOptionsType } from 'embla-carousel'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useEffect, useState } from 'react'
import Balancer from 'react-wrap-balancer'

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

export interface Carousel02Props {
  className?: string
  title?: string
  description?: string
  items: {
    title?: string
    description?: string
    media: {
      src: string
      alt: string
      overlay?: boolean
    }
    invert: boolean
  }[]
  showNavigation: boolean
  carouselOpts?: EmblaOptionsType
}

export function Carousel02({
  className,
  title,
  description,
  items,
  showNavigation,
}: Readonly<Carousel02Props>) {
  const [api, setApi] = useState<CarouselApi>()
  const [canScrollPrev, setCanScrollPrev] = useState(false)
  const [canScrollNext, setCanScrollNext] = useState(false)

  useEffect(() => {
    if (!api) {
      return
    }

    const onSelect = () => {
      setCanScrollPrev(api.canScrollPrev())
      setCanScrollNext(api.canScrollNext())
    }
    api.on('select', onSelect)
    api.on('reInit', onSelect)

    setTimeout(() => {
      onSelect()
    }, 0)

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

  const handlePrevious = () => {
    api?.scrollPrev()
  }

  const handleNext = () => {
    api?.scrollNext()
  }

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

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

  return (
    <div className="flex flex-col gap-8">
      <div
        className={cn(
          'flex w-full flex-col justify-between gap-6 md:flex-row md:items-center',
          containerWidthClassName,
        )}
      >
        <div className="flex flex-col gap-1">
          {title && (
            <h2 className="text-2xl font-bold md:max-w-200">
              <Balancer balance={0.5}>{title}</Balancer>
            </h2>
          )}
          {description && (
            <p className="text-muted-foreground md:max-w-200">
              <Balancer balance={0.5}>{description}</Balancer>
            </p>
          )}
        </div>
        {showNavigation && (
          <div className="flex items-center gap-2">
            <Button
              variant="outline"
              onClick={handlePrevious}
              disabled={!canScrollPrev}
              size="icon"
              className="bg-muted/60 hover:bg-muted h-10 w-10 rounded-full border-none shadow-none"
            >
              <ChevronLeft className="size-4" />
            </Button>
            <Button
              variant="outline"
              onClick={handleNext}
              disabled={!canScrollNext}
              size="icon"
              className="bg-muted/60 hover:bg-muted h-10 w-10 rounded-full border-none shadow-none"
            >
              <ChevronRight className="size-4" />
            </Button>
          </div>
        )}
      </div>
      {items && (
        <Carousel
          setApi={setApi}
          opts={{
            align: 'center',
            dragFree: true,
          }}
          className="w-full"
          aria-label="Carousel 02"
        >
          <CarouselContent
            className={cn(
              'h-auto select-none first:!pl-0',
              containerWidthClassName,
            )}
          >
            {items.map((item, index) => {
              return (
                <CarouselItem
                  key={`${item.title}-${index}`}
                  className="h-full basis-5/5 last:!pr-5 md:basis-2/5"
                >
                  <div className="relative flex w-full items-center justify-center">
                    <div className="group/media group/video relative w-full overflow-hidden rounded-xl">
                      {item.media && (
                        <>
                          <div className="relative h-full min-h-64 w-full md:min-h-96">
                            <img
                              src={item.media.src}
                              alt={item.title ?? 'Carousel 02 image'}
                              loading="lazy"
                              decoding="async"
                              className="absolute inset-0 size-full object-cover"
                            />
                          </div>
                          {item.media.overlay && (
                            <div className="absolute inset-0 bg-black/40" />
                          )}
                        </>
                      )}
                      <div className="absolute inset-0 z-10 flex items-center justify-center p-6">
                        <div className="max-w-2xl space-y-2 text-center">
                          {item.title && (
                            <h3
                              className={`text-xl font-medium ${
                                item.invert ? 'text-white' : ''
                              }`}
                            >
                              {item.title}
                            </h3>
                          )}
                          {item.description && (
                            <p
                              className={`text-sm ${
                                item.invert
                                  ? 'text-white'
                                  : 'text-muted-foreground'
                              }`}
                            >
                              {item.description}
                            </p>
                          )}
                        </div>
                      </div>
                    </div>
                  </div>
                </CarouselItem>
              )
            })}
          </CarouselContent>
        </Carousel>
      )}
    </div>
  )
}
```

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

```tsx
import { Carousel02, type Carousel02Props } from './carousel-02'

export const values = {
  title: 'In focus',
  description: 'Key themes in a compact, browsable format.',
  items: [
    {
      title: 'Ship faster',
      description: 'Less setup, more shipping.',
      media: {
        src: 'https://images.unsplash.com/photo-1596308501201-1d81619d907a?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
        overlay: true,
      },
      invert: true,
    },
    {
      title: 'Stay in control',
      description: 'Your code, your design system.',
      media: {
        src: 'https://images.unsplash.com/photo-1747370273882-20aefaa41dac?q=80&w=1074&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
        overlay: true,
      },
      invert: true,
    },
    {
      title: 'Built together',
      description: 'Open by default, improved by the community.',
      media: {
        src: 'https://images.unsplash.com/photo-1740073067432-0fd060f2ecde?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
        overlay: true,
      },
      invert: true,
    },
    {
      title: 'Performance first',
      description: 'Fast by default, without the tradeoffs.',
      media: {
        src: 'https://images.unsplash.com/photo-1567354440819-667147e4d12d?q=80&w=1167&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'alt image',
        overlay: true,
      },
      invert: true,
    },
  ],
  showNavigation: true,
} satisfies Carousel02Props

export function Carousel02Example() {
  return (
    <Carousel02
      title={values.title}
      description={values.description}
      items={values.items}
      showNavigation={values.showNavigation}
    />
  )
}
```
