WebberUI

Thumbnail Gallery

A gallery whose main image and thumbnail strip stay in sync, supporting drag to change images, arrow keys, slide/fade transitions, and a thumbnail strip on any of four sides.

Loading preview…
npx shadcn@latest add https://webberui.com/r/thumbnail-gallery.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<ThumbnailGallery />

Installation

npx shadcn@latest add https://webberui.com/r/thumbnail-gallery.json

Or, once registries are configured in components.json, install it as @webberui/thumbnail-gallery.

Usage

import { ThumbnailGallery } from "@/components/ui/thumbnail-gallery";

const images = [
  { src: "/photos/1.jpg", alt: "Sunset by the sea" },
  { src: "/photos/2.jpg", alt: "Forest trail", thumbnail: "/photos/2-thumb.jpg" },
  { src: "/photos/3.jpg", alt: "City at night" },
];

<ThumbnailGallery images={images} />;

Controlled mode

Pass value and onValueChange to hand the current index over to outside state:

const [index, setIndex] = React.useState(0);

<ThumbnailGallery images={images} value={index} onValueChange={setIndex} />;

Thumbnail position and transition effect

<ThumbnailGallery images={images} thumbnailPosition="left" effect="fade" />

Props

PropTypeDefaultDescription
imagesGalleryImage[]The list of images; the first item is the initial main image
valuenumberControlled current index
defaultValuenumber0Initial index in uncontrolled mode
onValueChange(index: number) => voidFires when the current index changes
thumbnailPosition"bottom" | "top" | "left" | "right""bottom"Position of the thumbnail strip (left/right make it a vertical strip)
effect"slide" | "fade""slide"Main image transition effect
loopbooleanfalseWhether to wrap around at the start and the end
showArrowsbooleantrueWhether to show the direction arrows on the main image
aspectRatiostring"4 / 3"Main image aspect ratio (a CSS aspect-ratio value)
dragThresholdnumber60Drag offset threshold that changes the image (px)
velocityThresholdnumber400Drag flick velocity threshold that changes the image (px/s)
labelstring"圖片畫廊"Overall accessibility label — the built-in default is Traditional Chinese for "image gallery", so pass this prop to localize it

GalleryImage

FieldTypeDescription
srcstringSource URL of the main image
altstringAlternative text (required for accessibility)
thumbnailstring | undefinedThumbnail source; falls back to src when omitted

How it works

  • The main image slides in (slide) or fades in (fade) through AnimatePresence depending on the direction of the change, keeping the previous and next images overlapping through the transition.
  • Press and drag on the main image (horizontally with a horizontal thumbnail strip, vertically with a vertical one), and crossing the offset or flick velocity threshold moves to the next or previous image; below the threshold it springs back.
  • The current thumbnail scrolls itself into the visible range of the thumbnail strip and centers, with the scrolling happening only inside the thumbnail strip container and never moving the whole page.
  • With loop on, the start and the end join up; with it off, the direction arrow at whichever end you have reached is disabled automatically.

Accessibility

  • The main image area is a focusable carousel; the arrow keys switch to the previous / next image, and Home / End jump to the first / last.
  • The thumbnail strip is a tablist with each thumbnail a tab, the current item marked with aria-selected and aria-current, and only the current item Tab focusable (roving tabindex).
  • A built-in aria-live="polite" region announces "image X of Y" plus the image's alternative text on each change.
  • When the user has "reduce motion" enabled at the system level, it switches instantly (no sliding, no fading) and the drag is disabled along with it.

On this page