SliderFrame

PreviousNext

A carousel-based frame for displaying multiple images with keyboard-accessible navigation controls

Mountain landscape 1
Mountain Resort
Winter Collection
'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
      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

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

Usage

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

PropTypeDefaultDescription
itemsImageProps[]RequiredArray of image props. A single item renders without carousel
labels{ next: string; previous: string }RequiredAccessible labels for the previous/next navigation buttons
describedbystringundefinedaria-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
classNamestringundefinedAdditional CSS classes
childrenReactNodeundefinedOverlay content (use FrameLabelsWrapper + FrameTitle)

Examples

With button

Mountain landscape 1
Mountain Resort
Winter
'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
      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

Mountain landscape 1
Best seller
Mountain Resort
Winter
'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
      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