docs / patterns / step-list
StepList
a vertical numbered process: a connector spine with a title and body per step. for how-it-works and onboarding sections.
intensity · structuresrsc · server
provenance
shaped from personal-websitelayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
a how-it-works section, alone or beside an illustrationwrap in RevealGroup for a staggered entrancenot a wizard. it explains a process, it does not run oneno long prose bodies. a step body is a sentence or twoa11y
an ordered list with a real heading per step · the connector spine isaria-hiddenjest-axelive demo · try it out
- 01
Sketch the flow
Rough the screens and the path between them.
- 02
Build the primitives
Wire the tokens and the core components.
- 03
Ship it
Push to the registry and migrate the apps.
customize
stepshow many in the sequence
bodiesa sentence under each title
iconsreplace the step number with an icon
headingLevellevel of each step title
usage
import { StepList } from "usva/patterns/step-list";
<StepList
headingLevel="h3"
steps={[
{ title: "Sketch the flow", body: "Rough the screens and the path between them." },
{ title: "Build the primitives", body: "Wire the tokens and the core components." },
{ title: "Ship it", body: "Push to the registry and migrate the apps." },
]}
/>props
| prop | type | default | notes |
|---|---|---|---|
| steps | Step[] | — | the steps: { title, body?, icon?, id? }. an icon replaces the number in the chip and moves the ordinal above the title. |
| headingLevel | "h2" | "h3" | "h4" | "h4" | level of each step title. pick it to fit the page outline. |
get it
npx shadcn add https://usva.build/r/step-list.jsonsource · components/ui/step-list.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export interface Step {
/** Optional icon; falls back to the mono step number. */
icon?: React.ReactNode;
title: React.ReactNode;
body?: React.ReactNode;
/** Stable key; defaults to the index. */
id?: string;
}
export interface StepListProps
extends Omit<React.HTMLAttributes<HTMLOListElement>, "children"> {
steps: Step[];
/** Level of each step title. Pick it to fit the page outline. */
headingLevel?: "h2" | "h3" | "h4";
}
export const StepList = React.forwardRef<HTMLOListElement, StepListProps>(
({ className, steps, headingLevel: Heading = "h4", ...props }, ref) => {
const last = steps.length - 1;
return (
<ol ref={ref} className={cn("flex flex-col", className)} {...props}>
{steps.map((step, i) => {
const key = step.id ?? `step-${i}`;
const ordinal = (i + 1).toString().padStart(2, "0");
return (
<li key={key} className="flex gap-4">
<div className="flex flex-col items-center">
<span className="grid size-11 shrink-0 place-items-center rounded-xl border border-border bg-surface-2 font-mono text-sm tabular-nums text-accent-alt [&_svg]:h-[1.15rem] [&_svg]:w-[1.15rem]">
{step.icon ?? ordinal}
</span>
{i < last && (
<span
aria-hidden="true"
className="mt-1 min-h-8 w-px flex-1 bg-border"
/>
)}
</div>
<div
className={cn(
"min-w-0",
step.body == null && "flex min-h-11 flex-col justify-center",
i < last ? "pb-8" : "pb-0",
)}
>
{step.icon != null && (
<p className="mb-1 font-mono text-[0.7rem] tabular-nums tracking-[0.16em] text-muted">
{ordinal}
</p>
)}
<Heading
className={cn(
"text-base font-semibold tracking-[-0.01em] text-ink",
step.body != null && "mb-1",
)}
>
{step.title}
</Heading>
{step.body != null && (
<p className="text-sm leading-relaxed text-muted">
{step.body}
</p>
)}
</div>
</li>
);
})}
</ol>
);
},
);
StepList.displayName = "StepList";