Ship-It Designv0.0.20

GitHub

Carousel

Horizontal scroll-snap container with prev/next controls and dot indicators. Native CSS scroll behavior — no library required. Use for listing photo galleries, marketing hero rotators, or feature walkthroughs.

Carousel is generic over the slide type — you pass items: T[] and a renderItem: (item, index) => ReactNode so cell rendering stays type-safe against your data.

The data shape#

tsx
<Carousel
  items={photos}
  renderItem={(src, i) => <img src={src} alt={`Photo ${i + 1}`} />}
  loop
  showDots
/>

For a thumbnail strip below the main viewport, pass renderThumbnail with the same (item, index) => ReactNode signature.

Default#

Loading…

With thumbnails#

Listing detail pages typically pair the main viewport with a thumbnail strip. Each thumbnail is rendered by renderThumbnail and clicking one jumps the main viewport to that index.

Loading…

Loop#

Set loop to make arrows, dots, and native swipe wrap continuously — "next" on the last slide goes to the first, and vice versa. Under the hood, hidden clones of the first / last slide bracket the real items so the swipe-past-the-edge feels seamless; onIndexChange still only emits real indices in 0..items.length - 1. The marketplace patterns (ListingCard, ListingDetail) opt in by default.

loop accepts two variants — pass true to get the default ("circular"), or "sweep" for the alternate motion. Native swipe past the edge looks the same in both; the variants only differ on the prev/next-arrow wrap step.

Loop modes (loop: boolean | 'circular' | 'sweep')#

  • false (default) — stop at the boundary. The prev/next arrow on the first/last slide is disabled.
  • true or 'circular' — boundary arrow clicks smooth-scroll a single slide width through the hidden clone of the opposite end, then invisibly snap to the real twin. Feels like an endless reel — the motion is always one slide forward / back, regardless of strip length.
  • 'sweep' — boundary arrow clicks smooth-scroll the full distance across the strip back to the real first / last slide. The transition reads as a wide arc across every item between — useful when you want the user to re-perceive the intermediate slides on each wrap.

Circular (default)#

Loading…

Sweep#

Loading…

Active index#

The active slide is either controlled via index + onIndexChange or uncontrolled via defaultIndex (default 0). Controlled mode lets you sync the carousel with parent state (e.g. drive it from a keyboard shortcut elsewhere, or persist position to a URL).

onIndexChange always fires with a real index in 0..items.length - 1 — never the clone indices used internally for the loop machinery. Use this for analytics or for syncing a sibling component like ListingDetail's gallery↔lightbox shared index.

Top-level props#

Props for Carousel
PropTypeDefaultDescription
items *readonly T[]Slide data.
renderItem *(item: T, index: number) => ReactNodeRenderer for each slide.
renderThumbnail((item: T, index: number) => ReactNode) | undefinedOptional renderer for a thumbnail strip below the viewport.
indexnumber | undefinedActive slide index (controlled).
defaultIndexnumber | undefinedDefault index (uncontrolled). Default `0`.
onIndexChange((index: number) => void) | undefinedFires when the active index changes.
aspectRatiostring | number | undefinedAspect ratio of each slide. Default `16/10`.
showDotsboolean | undefinedWhen false, hides the dot indicators. Default `true`.
showArrowsboolean | undefinedWhen false, hides the prev/next arrows. Default `true`.
loopboolean | "circular" | "sweep" | undefinedWrap arrows / dots / native swipe past the boundaries. Default `false`. Variants: - `"circular"` (or `true`): boundary arrow clicks smooth-scroll a single slide width through a hidden clone of the opposite end, then invisibly snap to the real twin. Feels like an endless reel — the motion is always one slide, regardless of strip length. - `"sweep"`: boundary arrow clicks smooth-scroll the full distance across the strip back to the real first / last slide. The transition reads as a wide arc across every item between. Native swipe past the edge always uses the clone-snap (independent of variant). `onIndexChange` only emits real indices in `0..items.length - 1`.
aria-labelstring | undefinedAccessible label for the carousel region.
refRef<HTMLDivElement> | undefined

Accessibility#

  • The viewport is role="region" with aria-roledescription="carousel".
  • Each slide is a role="group" with aria-label="N of M".
  • Dot indicators are a role="tablist" so screen readers announce "selected" / "not selected".
  • Keyboard: focus the viewport and use Arrow Left / Right to advance. The prev/next buttons are focusable as well.
  • Honor prefers-reduced-motion: the scroll uses behavior: smooth which collapses to instant under reduced motion via the global CSS rule.