Components · Navigation
Steps
Progress through a flow: done, current (aria-current=step) and to come; horizontal when there is room.
0.68% of the 2,062 sites in The UI Index have a stepper (14 sites).
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ steps.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ steps.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
import { cn } from '@/lib/utils';
import { CHECK_PATH, steps as c, type Step } from './steps.variants';
export type { Step };
export interface StepsProps {
steps: Step[];
/** The step you are on, counted from 0. Steps before it are done. */
current: number;
label?: string;
className?: string;
}
/** Progress through a flow: done, current (aria-current="step") and still to come. */
export function Steps({ steps, current, label = 'Progress', className }: StepsProps) {
return (
<nav aria-label={label} className={cn(c.root, className)}>
<ol className={c.list}>
{steps.map((s, k) => {
const state = k < current ? 'done' : k === current ? 'current' : 'todo';
return (
<li key={k} data-state={state} aria-current={state === 'current' ? 'step' : undefined} className={c.item}>
<span className={c.rail}>
<span className={c.mark}>
{state === 'done' ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={CHECK_PATH} /></svg> : k + 1}
</span>
<span className={c.line} aria-hidden="true"></span>
</span>
<span className={c.text}>
<span className={c.title}>{s.title}<span className="sr-only">{state === 'done' ? ' (done)' : state === 'current' ? ' (current)' : ''}</span></span>
{s.description && <span className={c.description}>{s.description}</span>}
</span>
</li>
);
})}
</ol>
</nav>
);
}/** Progress through a flow: an ordered list of steps, done (a check), current (aria-current="step", ink) and still
* to come. A real sequence, so the numbers are earned. Horizontal from a wide container, vertical on phones. */
export const steps = {
root: '@container w-full',
list: 'm-0 grid list-none gap-4 p-0 @xl:flex @xl:items-start @xl:gap-0',
item: 'group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2',
rail: 'flex items-center @xl:w-full',
mark: 'grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5',
line: 'hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible',
text: 'min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4',
title: 'block text-sm font-medium group-data-[state=todo]:text-muted-foreground',
description: 'mt-0.5 block text-sm text-pretty text-muted-foreground',
};
export type Step = { title: string; description?: string };
export const CHECK_PATH = 'M3.5 8.5l3 3 6-7';---
import { cn } from '@/lib/utils';
import { CHECK_PATH, steps as c, type Step } from './steps.variants';
interface Props {
steps: Step[];
/** The step you are on, counted from 0. Steps before it are done. */
current: number;
label?: string;
class?: string;
}
const { steps, current, label = 'Progress', class: className } = Astro.props;
---
<nav aria-label={label} class={cn(c.root, className)}>
<ol class={c.list}>
{
steps.map((s, k) => {
const state = k < current ? 'done' : k === current ? 'current' : 'todo';
return (
<li data-state={state} aria-current={state === 'current' ? 'step' : undefined} class={c.item}>
<span class={c.rail}>
<span class={c.mark}>
{state === 'done' ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={CHECK_PATH} /></svg> : k + 1}
</span>
<span class={c.line} aria-hidden="true"></span>
</span>
<span class={c.text}>
<span class={c.title}>{s.title}<span class="sr-only">{state === 'done' ? ' (done)' : state === 'current' ? ' (current)' : ''}</span></span>
{s.description && <span class={c.description}>{s.description}</span>}
</span>
</li>
);
})
}
</ol>
</nav>/** Progress through a flow: an ordered list of steps, done (a check), current (aria-current="step", ink) and still
* to come. A real sequence, so the numbers are earned. Horizontal from a wide container, vertical on phones. */
export const steps = {
root: '@container w-full',
list: 'm-0 grid list-none gap-4 p-0 @xl:flex @xl:items-start @xl:gap-0',
item: 'group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2',
rail: 'flex items-center @xl:w-full',
mark: 'grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5',
line: 'hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible',
text: 'min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4',
title: 'block text-sm font-medium group-data-[state=todo]:text-muted-foreground',
description: 'mt-0.5 block text-sm text-pretty text-muted-foreground',
};
export type Step = { title: string; description?: string };
export const CHECK_PATH = 'M3.5 8.5l3 3 6-7';<nav aria-label="New project" class="@container w-full max-w-3xl">
<ol class="m-0 grid list-none gap-4 p-0 @xl:flex @xl:items-start @xl:gap-0">
<li data-state="done" class="group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2">
<span class="flex items-center @xl:w-full">
<span class="grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"></path></svg>
</span>
<span class="hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible" aria-hidden="true"></span>
</span>
<span class="min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4">
<span class="block text-sm font-medium group-data-[state=todo]:text-muted-foreground">Account<span class="sr-only"> (done)</span></span>
<span class="mt-0.5 block text-sm text-pretty text-muted-foreground">Your name and email.</span>
</span>
</li>
<li data-state="done" class="group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2">
<span class="flex items-center @xl:w-full">
<span class="grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"></path></svg>
</span>
<span class="hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible" aria-hidden="true"></span>
</span>
<span class="min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4">
<span class="block text-sm font-medium group-data-[state=todo]:text-muted-foreground">Repository<span class="sr-only"> (done)</span></span>
<span class="mt-0.5 block text-sm text-pretty text-muted-foreground">Pick what to deploy.</span>
</span>
</li>
<li data-state="current" aria-current="step" class="group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2">
<span class="flex items-center @xl:w-full">
<span class="grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5">
3
</span>
<span class="hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible" aria-hidden="true"></span>
</span>
<span class="min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4">
<span class="block text-sm font-medium group-data-[state=todo]:text-muted-foreground">Domain<span class="sr-only"> (current)</span></span>
<span class="mt-0.5 block text-sm text-pretty text-muted-foreground">Where it goes live.</span>
</span>
</li>
<li data-state="todo" class="group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2">
<span class="flex items-center @xl:w-full">
<span class="grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5">
4
</span>
<span class="hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible" aria-hidden="true"></span>
</span>
<span class="min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4">
<span class="block text-sm font-medium group-data-[state=todo]:text-muted-foreground">Review<span class="sr-only"></span></span>
<span class="mt-0.5 block text-sm text-pretty text-muted-foreground">Check and deploy.</span>
</span>
</li>
</ol>
</nav>