Carousels

Velocity Scene Slider

A scene-card slider whose release velocity becomes the next card's spring velocity, with a stretching pill indicator.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

A horizontal track of scene cards: each card holds any content, and you switch by dragging sideways or tapping the pill indicator. The interesting part is the release — the drag velocity is handed straight to the page-change spring as its initial velocity, so a fast flick lands fast and a slow push glides over, one continuous velocity curve from finger to rest. While dragging, the pills stretch and shrink with progress, and cards sharpen as they reach the centre. Good for feature tours, onboarding and product walkthroughs.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/velocity-scene-slider.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

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

260
30
400
Active edited usageDefault props
<VelocitySceneSlider/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/velocity-scene-slider.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/velocity-scene-slider.

Usage

import {
  VelocitySceneSlider,
  type VelocityScene,
} from "@/components/ui/velocity-scene-slider";

const scenes: VelocityScene[] = [
  { id: "inbox", label: "Inbox triage", content: <InboxScene /> },
  { id: "schedule", label: "Scheduling", content: <ScheduleScene /> },
  { id: "board", label: "Team board", content: <BoardScene /> },
];

export function Tour() {
  const [index, setIndex] = React.useState(0);
  return (
    <VelocitySceneSlider
      scenes={scenes}
      index={index}
      onIndexChange={setIndex}
      onRelease={({ velocity }) => console.log(`${Math.round(velocity)} px/s`)}
    />
  );
}

Props

PropTypeDefaultDescription
scenesVelocityScene[]—Scene cards; 3–7 works best
indexnumber—Current scene index in controlled mode; managed internally when omitted
defaultIndexnumber0Initial scene index in uncontrolled mode
onIndexChange(index: number) => void—Called when the scene changes (drag, indicator and keyboard all report)
onRelease(info: VelocitySceneRelease) => void—Called on drag release with the velocity, offset and target index
stiffnessnumber260Page-change spring stiffness; higher settles faster
dampingnumber30Page-change spring damping; lower overshoots more
velocityThresholdnumber400Release velocity (px/s) above which the page changes regardless of distance
loopbooleanfalseWrap around to the other end at the first/last scene
showIndicatorbooleantrueShow the pill indicator below the track
ariaLabelstring"Scene carousel"Accessible label for the whole carousel
classNamestring—Passed through to the root element

VelocityScene

FieldTypeDefaultDescription
idstring—Unique scene id
labelstring—Scene name, used by the indicator buttons and screen readers
contentReact.ReactNode—Card content

VelocitySceneRelease

FieldTypeDefaultDescription
velocitynumber—Horizontal velocity at release (px/s, negative = leftward)
offsetnumber—Horizontal offset travelled during the drag (px)
indexnumber—Scene index the slider heads to after release

CSS variables

VariableDefaultDescription
--wb-velocity-scene-indicator-duration240msTransition time for pill width and colour
--wb-velocity-scene-gap24pxGap between scene cards; the step is measured from layout, so dragging and positioning follow any override

How it works

  • Velocity matching: on release the drag velocity is passed unchanged as the page-change spring's velocity. With the same stiffness / damping, a flick and a slow push therefore arrive at different speeds and feel different; indicator and keyboard navigation start from velocity 0
  • Page-change rule: velocity wins — if the absolute release velocity exceeds velocityThreshold, the slider moves one page in that direction; otherwise the drag must pass 25% of the card width, or it springs back
  • Wrapping: with loop on, flicking past the last card returns to the first. The track then travels far in the opposite direction, and carrying the release velocity would shoot it the wrong way first, so wrapping starts from velocity 0
  • Indicator: at rest the active pill is 28px and the others 8px, with a CSS transition; while dragging, the two neighbouring widths are interpolated every frame from continuous progress, so the pill looks stretched
  • Scene cards: scale 0.94→1 and opacity 0.6→1 interpolate from the distance to centre; cards are 82% of the container width so neighbours peek in on both sides
  • Controlled mode: if the parent does not accept the new index from onIndexChange, the track springs back to the original scene

Accessibility

  • The root is role="region" with aria-roledescription="carousel", focusable with Tab and showing a focus-visible ring
  • Each card is role="group" with aria-roledescription="slide" and an aria-label of "Scene n of N: name"; off-centre cards are inert, so they are neither focusable nor announced
  • Once focused, Left/Right arrows change page and Home/End jump to the first/last scene
  • The indicator is a role="tablist"; each pill is a type="button" tab with aria-selected and aria-controls, using a roving tabindex so focus follows arrow-key moves
  • The current scene is announced through aria-live="polite"
  • With the system "reduce motion" setting on, the post-release spring momentum is disabled and page changes become a 150ms total fade out/in; the pills jump straight to their target width without a transition