Skip to content

Step player

An iOS-style stepped progress track with a play, pause and replay control. The active step stretches into a bar that fills as it plays.

Step 1 of 5

Installation

  1. 1

    Install the packages

    pnpm add motion flubber clsx tailwind-merge
  2. 2

    Add the utility file

    lib/utils.ts
    import { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";
    export function cn(...inputs: ClassValue[]) {  return twMerge(clsx(inputs));}
  3. 3

    Copy and paste the code into your project

    Loading source…
  4. 4

    Update the import paths to match your project

    Point @/lib/utils to your utility file if your path aliases differ.

Usage

app/page.tsx
import { StepPlayer } from "@/components/ui/step-player"
export function Demo() {  return <StepPlayer steps={5} duration={3000} />}
// drive your own carousel off the index<StepPlayer  steps={slides.length}  value={index}  onValueChange={setIndex}  loop/>

Options

Pick a value to see the prop you would pass. Defaults are left out.

controlPosition
snippet.tsx
<StepPlayer />

Props

PropTypeDefaultDescription
stepsnumber | StepPlayerStep[]Default: 4How many steps to show. Pass an array to give each one its own duration or label.
valuenumberDefault: -Active step index, for controlled use. Leave it out and the component tracks its own index.
defaultValuenumberDefault: 0Step to start on when uncontrolled. Clamped to the last step.
onValueChange(value: number) => voidDefault: -Fires when the step advances, wraps, or is clicked. Also fires on replay.
playingbooleanDefault: -Play state, for controlled use. Leave it out to let the button drive it.
defaultPlayingbooleanDefault: falseWhether the sequence starts running on mount when uncontrolled.
onPlayingChange(playing: boolean) => voidDefault: -Fires when the button is pressed, and when the last step ends without loop.
durationnumberDefault: 4000Milliseconds each step runs for. A per-step duration overrides it; 0 stops the timer so you can drive the index yourself.
loopbooleanDefault: falseWraps back to the first step instead of stopping. The replay button never appears when this is on.
onComplete() => voidDefault: -Fires when the last step finishes, on every pass when looping.
sizenumberDefault: 48Height of the track and button in pixels. Every other dimension is a fixed ratio of it, so the dots, bar, gaps, padding, icon, and tap targets all scale from this one number.
showControlbooleanDefault: trueShows the round play button. Turn it off for a track on its own and drive it with the playing prop.
controlPosition"left" | "right"Default: "right"Which side of the track the button sits on.
seekablebooleanDefault: falseLets a step be clicked to jump to it. The dots keep a finger-sized tap target at every size.
classNamestringDefault: -Extra classes merged onto the root element. It sizes itself from the size prop, so give it a width only if you want the track wider than its content.

Press play to start the sequence. The current step stretches into a bar and fills as it runs, then shrinks to a filled dot when it hands off to the next one. Press again to pause mid-fill; the play icon splits into the pause bars as it goes. At the end it fades into a replay arrow. Steps can be clicked to jump.

Credits and license

Most components here are recreations of great work from around the web. I don't claim to be the original creator - this is my attempt to reverse-engineer, replicate, and often add a few extra features. I've tried to credit everyone; if I missed someone, let me know.

  • Free to use and modify in both personal and commercial projects.
  • Attribution to Mox UI is appreciated when using a component.
  • Please don't resell the components as your own kit.
Contact Mox UI