Steps
A controlled steps indicator with completed, current and upcoming states, and forward-jump locking for wizard flows
Installation
- $npx litefy@latest add steps
- $pnpm dlx litefy@latest add steps
- $yarn dlx litefy@latest add steps
- $bun --bun litefy@latest add steps
Usage
Fully controlled index; completed and upcoming steps are clickable and fire onChange, the current step is not.
- AccountBasic info
- Details
- Settings
- Review
"use client";
import * as React from "react";
import { Button, Steps } from "@/ui/components";
const items = [
{ title: "Account", description: "Basic info" },
{ title: "Profile", description: "Details" },
{ title: "Preferences", description: "Settings" },
{ title: "Confirm", description: "Review" },
];
export default function StepsBasicDemo() {
const [index, setIndex] = React.useState(0);
return (
<div className="w-full space-y-6">
<Steps items={items} index={index} onChange={setIndex} />
<div className="flex items-center justify-between">
<Button
variant="outline"
onClick={() => setIndex((i) => Math.max(0, i - 1))}
disabled={index === 0}
>
Back
</Button>
<span className="text-sm text-muted-foreground">Step {index + 1} of {items.length}</span>
<Button
variant="outline"
onClick={() => setIndex((i) => Math.min(items.length - 1, i + 1))}
disabled={index === items.length - 1}
>
Next
</Button>
</div>
</div>
);
}
Locked Navigation
Set maxIndex to the highest visited step — completed steps stay clickable, future steps are locked.
- AccountBasic info
- ProfileDetails
- PreferencesSettings
- ConfirmReview
Visited steps stay clickable — future steps stay locked until reached.
"use client";
import * as React from "react";
import { Button, Steps } from "@/ui/components";
const items = [
{ title: "Account", description: "Basic info" },
{ title: "Profile", description: "Details" },
{ title: "Preferences", description: "Settings" },
{ title: "Confirm", description: "Review" },
];
export default function StepsLockedDemo() {
const [index, setIndex] = React.useState(0);
const [maxIndex, setMaxIndex] = React.useState(0);
const go = (next: number) => {
setIndex(next);
setMaxIndex((m) => Math.max(m, next));
};
return (
<div className="w-full space-y-6">
<Steps items={items} index={index} maxIndex={maxIndex} onChange={go} />
<p className="text-sm text-muted-foreground">
Visited steps stay clickable — future steps stay locked until reached.
</p>
<div className="flex items-center justify-between">
<Button
variant="outline"
onClick={() => go(Math.max(0, index - 1))}
disabled={index === 0}
>
Back
</Button>
<span className="text-sm text-muted-foreground">Step {index + 1} of {items.length}</span>
<Button
variant="outline"
onClick={() => go(Math.min(items.length - 1, index + 1))}
disabled={index === items.length - 1}
>
Next
</Button>
</div>
</div>
);
}
Vertical
Set orientation="vertical" to stack the steps — titles and descriptions render beside the markers, matching the Timeline look.
- AccountCreate your login
- Tell us about you
- Review and submit
"use client";
import * as React from "react";
import { Button, Steps } from "@/ui";
const content = [
"Create your login credentials.",
"Tell us a bit about yourself.",
"Review everything and submit.",
];
export default function Demo() {
const [index, setIndex] = React.useState(0);
return (
<div className="w-full max-w-sm space-y-4">
<Steps
orientation="vertical"
index={index}
onChange={setIndex}
items={[
{ title: "Account", description: "Create your login" },
{ title: "Profile", description: "Tell us about you" },
{ title: "Confirm", description: "Review and submit" },
]}
/>
<div className="rounded-lg border p-3 text-sm text-muted-foreground">{content[index]}</div>
<div className="flex items-center justify-between">
<Button
variant="outline"
onClick={() => setIndex((i) => Math.max(0, i - 1))}
disabled={index === 0}
>
Back
</Button>
<Button
variant="outline"
onClick={() => setIndex((i) => Math.min(2, i + 1))}
disabled={index === 2}
>
Next
</Button>
</div>
</div>
);
}
API Reference
Steps
| Prop | Type | Default | Description |
|---|---|---|---|
items | StepsItemConfig[] | - | Step definitions |
index | number | - | Controlled current step index |
onChange | (nextIndex: number) => void | - | Fired when a clickable step is selected |
maxIndex | number | items.length - 1 | Highest selectable index — steps beyond it are disabled |
orientation | "horizontal" | "vertical" | "horizontal" | "vertical" stacks the steps with titles beside the markers |
className | ClassNameValue | - | Custom class names, applied to the root |
classNames | marker / title / description / connector | - | Custom class names per slot |
StepsItemConfig
| Field | Type | Default | Description |
|---|---|---|---|
title | React.ReactNode | - | Step title |
description | React.ReactNode | - | Optional secondary text |
Composable Components
Parts merge their built-in className with yours — passing className extends the defaults instead of replacing them.
StepsRoot
The outer <ol> flex container (role list semantics come from the native element).
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"ol"> | - | Native ol props |
StepsItem
A single step: connector line, circular marker button, title and optional description; the marker is keyboard accessible when clickable.
| Prop | Type | Default | Description |
|---|---|---|---|
index | number | - | Zero-based step index, reported through onSelect |
state | "completed" | "current" | "upcoming" | - | Visual state |
title | React.ReactNode | - | Step title |
description | React.ReactNode | - | Optional secondary text |
clickable | boolean | - | Whether the marker button is enabled |
onSelect | (index: number) => void | - | Fired on marker click |
className | ClassNameValue | - | Custom class names, applied to the item |
classNames | marker / title / description / connector | - | Custom class names per slot |
...props | React.ComponentProps<"li"> | - | Native li props |