---
title: SliderFrame
description: A carousel-based frame for displaying multiple images with keyboard-accessible navigation controls
---

```tsx
'use client';

import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';
import { SliderFrame } from '@/ui/frame/SliderFrame';

export function SliderFrameDemo() {
  return (
    <SliderFrame
      aria-label="Mountain Resort image carousel"
      format="horizontal"
      className="max-w-380"
      items={Array.from({ length: 5 }, (_, i) => ({
        alt: `Mountain landscape ${i + 1}`,
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }))}
      labels={{ next: 'Next image', previous: 'Previous image' }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-center text-center" hasImage>
          <div>
            <div className="text-b2 font-semibold">Mountain Resort</div>
            <div className="text-b3">Winter Collection</div>
          </div>
        </FrameTitle>
      </FrameLabelsWrapper>
    </SliderFrame>
  );
}
```

## Overview

`SliderFrame` extends `FrameWrapper` with a carousel of images. When multiple items are provided it renders an embla-powered carousel with previous/next buttons that appear on hover. When only one item is provided, it renders a static image without carousel overhead.

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/slider-frame.json
```

Or manually, copy the following code into `/ui/frame/SliderFrame.tsx`:

```tsx
'use client';
import { type FunctionComponent, useMemo } from 'react';

import { Image, type ImageProps } from '../Image';
import { Carousel } from '../carousel/Carousel';
import { CarouselBody } from '../carousel/CarouselBody';
import { CarouselItem } from '../carousel/CarouselItem';
import { CarouselNext } from '../carousel/CarouselNext';
import { CarouselPrevious } from '../carousel/CarouselPrevious';

import { FrameWrapper, type FrameWrapperProps } from './FrameWrapper';

interface SliderLabels {
  next: string;
  previous: string;
}

export interface SliderFrameProps extends FrameWrapperProps {
  describedby?: string;
  items: ImageProps[];
  labels: SliderLabels;
}

export const SliderFrame: FunctionComponent<SliderFrameProps> = ({
  children,
  describedby,
  items,
  labels,
  ...props
}) => {
  const hasScroll = items.length > 1;

  const sliderItems = useMemo(
    () =>
      items.map((item) => (
        <Image key={item.src} {...item} className="pointer-events-none size-full object-cover" />
      )),
    [items],
  );

  return (
    <FrameWrapper {...props}>
      {hasScroll ? (
        <Carousel className="rounded-16 h-full select-none">
          <CarouselBody className="h-full" scrollClassName="h-full auto-cols-full">
            {sliderItems.map((item, i) => (
              <CarouselItem index={i} key={i}>
                <div className="h-full overflow-hidden">{item}</div>
              </CarouselItem>
            ))}
          </CarouselBody>
          <div className="pointer-events-none absolute inset-0 z-1 -mx-70 flex items-center justify-between gap-x-20 px-12 opacity-0 transition-all group-hover:mx-0 group-hover:opacity-100">
            <CarouselPrevious aria-describedby={describedby} label={labels.previous} />
            <CarouselNext aria-describedby={describedby} label={labels.next} />
          </div>
        </Carousel>
      ) : (
        <Image {...items[0]} className="size-full object-cover" />
      )}
      {children}
    </FrameWrapper>
  );
};
```

## Usage

```tsx showLineNumbers
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';
import { SliderFrame } from '@/ui/frame/SliderFrame';

export function Component() {
  return (
    <SliderFrame
      format="horizontal"
      className="max-w-380"
      items={[
        { alt: 'Image 1', src: '/images/photo-1.jpg' },
        { alt: 'Image 2', src: '/images/photo-2.jpg' },
        { alt: 'Image 3', src: '/images/photo-3.jpg' },
      ]}
      labels={{ next: 'Next image', previous: 'Previous image' }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-center text-center" hasImage>
          <div className="text-b2 font-semibold">Mountain Resort</div>
        </FrameTitle>
      </FrameLabelsWrapper>
    </SliderFrame>
  );
}
```

## API Reference

### SliderFrame

| Prop          | Type                                                | Default        | Description                                                          |
| ------------- | --------------------------------------------------- | -------------- | -------------------------------------------------------------------- |
| `items`       | `ImageProps[]`                                      | Required       | Array of image props. A single item renders without carousel         |
| `labels`      | `{ next: string; previous: string }`                | Required       | Accessible labels for the previous/next navigation buttons           |
| `describedby` | `string`                                            | `undefined`    | `aria-describedby` id passed to the navigation buttons               |
| `format`      | `"horizontal" \| "vertical" \| "square" \| "custom"` | `"horizontal"` | Aspect ratio of the frame                                            |
| `as`          | `"article" \| "div" \| "span"`                     | `"article"`    | HTML element to render                                               |
| `className`   | `string`                                            | `undefined`    | Additional CSS classes                                               |
| `children`    | `ReactNode`                                         | `undefined`    | Overlay content (use FrameLabelsWrapper + FrameTitle)                |

## Examples

### With button

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';
import { SliderFrame } from '@/ui/frame/SliderFrame';

export function SliderFrameWithButtonDemo() {
  return (
    <SliderFrame
      aria-label="Mountain Resort image carousel"
      format="horizontal"
      className="max-w-380"
      items={Array.from({ length: 5 }, (_, i) => ({
        alt: `Mountain landscape ${i + 1}`,
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }))}
      labels={{ next: 'Next image', previous: 'Previous image' }}
    >
      <FrameLabelsWrapper hasImage>
        <FrameTitle className="w-full justify-between" hasImage>
          <div>
            <div className="text-b2 font-semibold">Mountain Resort</div>
            <div className="text-b3">Winter</div>
          </div>
          <Button
            aria-label="Explore Mountain Resort"
            className="pointer-events-auto shrink-0"
            color="white"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </SliderFrame>
  );
}
```

### With tag

```tsx
'use client';

import { Button } from '@/ui/buttons/Button';
import { Tag } from '@/ui/Tag';
import { FrameLabelsWrapper } from '@/ui/frame/FrameLabelsWrapper';
import { FrameTitle } from '@/ui/frame/FrameTitle';
import { SliderFrame } from '@/ui/frame/SliderFrame';

export function SliderFrameWithTagDemo() {
  return (
    <SliderFrame
      aria-label="Mountain Resort image carousel"
      format="horizontal"
      className="max-w-380"
      items={Array.from({ length: 5 }, (_, i) => ({
        alt: `Mountain landscape ${i + 1}`,
        src: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
      }))}
      labels={{ next: 'Next image', previous: 'Previous image' }}
    >
      <FrameLabelsWrapper hasImage>
        <Tag
          label="Best seller"
          className="mx-12 mt-12 self-start px-8 sm:mx-24 sm:mt-24 sm:px-16"
        />
        <FrameTitle className="w-full justify-between" hasImage>
          <div>
            <div className="text-b2 font-semibold">Mountain Resort</div>
            <div className="text-b3">Winter</div>
          </div>
          <Button
            aria-label="Explore Mountain Resort"
            className="pointer-events-auto shrink-0"
            color="white"
            icon="ArrowDefaultRight"
            theme="outline"
            variant="circle"
          />
        </FrameTitle>
      </FrameLabelsWrapper>
    </SliderFrame>
  );
}
```

## Notes

- Navigation buttons are hidden by default and revealed on `group-hover` using Tailwind transitions
- The carousel uses embla-carousel with wheel gesture support
- A single-item `items` array skips the carousel entirely and renders a plain `<img>` — no embla overhead
- Use `labels` to provide translated strings for accessibility; the button text is visually hidden (`sr-only` via ArrowButton)
- `describedby` connects the navigation buttons to a visible label elsewhere on the page for screen readers
- The component is marked `'use client'` due to embla's hooks
