Navigation
Stepper.
Numbered steps with zero-padded mono numbers in bordered squares. Complete steps turn green with a check and the current step is orange.
- Completed: Place a computerAny advanced computer with a wired or wireless modem attached.
- Current step: Run the installerType
pastebin run Xk2p9at the shell. - Not started: Claim a hub addressPick a name so other players can find your site.
- Not started: PublishEdit index.lua and reboot to go live.
import { Stepper, StepperDescription, StepperItem, StepperTitle } from "@/components/ui/stepper";
export default function StepperDemo() {
return (
<Stepper className="w-full max-w-md">
<StepperItem state="complete">
<StepperTitle>Place a computer</StepperTitle>
<StepperDescription>Any advanced computer with a wired or wireless modem attached.</StepperDescription>
</StepperItem>
<StepperItem state="current">
<StepperTitle>Run the installer</StepperTitle>
<StepperDescription>
Type <code className="rounded-sm bg-secondary px-1 py-0.5 font-mono text-[12px] text-foreground">pastebin run Xk2p9</code> at the shell.
</StepperDescription>
</StepperItem>
<StepperItem>
<StepperTitle>Claim a hub address</StepperTitle>
<StepperDescription>Pick a name so other players can find your site.</StepperDescription>
</StepperItem>
<StepperItem>
<StepperTitle>Publish</StepperTitle>
<StepperDescription>Edit index.lua and reboot to go live.</StepperDescription>
</StepperItem>
</Stepper>
);
}Installation#
With the CLI
$npx lanterncn add stepperThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/stepper. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install lucide-reactCopy the source into your project
components/ui/stepper.tsx"use client"; import * as React from "react"; import { CheckIcon } from "lucide-react"; import { cn } from "@/lib/utils"; type StepperOrientation = "vertical" | "horizontal"; type StepState = "complete" | "current" | "upcoming"; const StepperContext = React.createContext<StepperOrientation>("vertical"); /** * Numbered steps. Horizontal steppers fall back to vertical when their container * is narrower than 36rem. */ function Stepper({ className, orientation = "vertical", children, ...props }: React.ComponentProps<"ol"> & { orientation?: StepperOrientation }) { const list = ( <ol data-slot="stepper" data-orientation={orientation} className={cn("flex flex-col [counter-reset:step]", orientation === "horizontal" && "@xl:flex-row @xl:gap-4", className)} {...props} > {children} </ol> ); return ( <StepperContext.Provider value={orientation}> {orientation === "horizontal" ? <div className="@container w-full">{list}</div> : list} </StepperContext.Provider> ); } const stateLabel: Record<StepState, string> = { complete: "Completed", current: "Current step", upcoming: "Not started" }; function StepperItem({ className, state = "upcoming", children, ...props }: React.ComponentProps<"li"> & { state?: StepState }) { const horizontal = React.useContext(StepperContext) === "horizontal"; return ( <li data-slot="stepper-item" data-state={state} aria-current={state === "current" ? "step" : undefined} className={cn( "group/step relative flex gap-4 pb-8 [counter-increment:step] last:pb-0", horizontal && "@xl:flex-1 @xl:flex-col @xl:gap-3 @xl:pb-0", className, )} {...props} > <span aria-hidden="true" data-slot="stepper-line" className={cn( "absolute top-9 bottom-1 left-[13.5px] w-px bg-border group-last/step:hidden group-data-[state=complete]/step:bg-success/50", horizontal && "@xl:top-3.5 @xl:right-2 @xl:bottom-auto @xl:left-10 @xl:h-px @xl:w-auto", )} /> <span data-slot="stepper-indicator" className={cn( "relative z-10 flex size-7 shrink-0 items-center justify-center rounded-md border border-input bg-background font-mono text-[11px] text-muted-foreground", "before:content-[counter(step,decimal-leading-zero)]", "group-data-[state=current]/step:border-primary group-data-[state=current]/step:bg-primary/10 group-data-[state=current]/step:text-primary group-data-[state=current]/step:shadow-[3px_3px_0_var(--block-shadow)]", "group-data-[state=complete]/step:border-success/60 group-data-[state=complete]/step:bg-success/10 group-data-[state=complete]/step:text-success group-data-[state=complete]/step:before:hidden", )} > {state === "complete" && <CheckIcon className="size-3.5" aria-hidden="true" />} </span> <span className="sr-only">{stateLabel[state]}: </span> <div data-slot="stepper-content" className="grid min-w-0 flex-1 content-start gap-1 pt-0.5"> {children} </div> </li> ); } function StepperTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="stepper-title" className={cn( "font-display text-base leading-snug font-medium tracking-tight group-data-[state=upcoming]/step:text-muted-foreground", className, )} {...props} /> ); } function StepperDescription({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="stepper-description" className={cn("text-sm text-muted-foreground", className)} {...props} />; } export { Stepper, StepperItem, StepperTitle, StepperDescription, type StepState };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { Stepper, StepperItem, StepperTitle, StepperDescription } from "@/components/ui/stepper";<Stepper>
<StepperItem state="complete">
<StepperTitle>Place a computer</StepperTitle>
<StepperDescription>Attach a modem.</StepperDescription>
</StepperItem>
<StepperItem state="current">
<StepperTitle>Run the installer</StepperTitle>
</StepperItem>
</Stepper>Numbers come from a CSS counter, so steps number themselves. The horizontal orientation uses a container query and stacks vertically when its container is narrower than 36rem. The current step gets aria-current="step".
Examples#
Horizontal with controls
- Completed: FuelLoad coal into slot 16.
- Current step: RouteSet the mining depth.
- Not started: DeploySend the turtle out.
Step 2 of 3
"use client";
import * as React from "react";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Stepper, StepperDescription, StepperItem, StepperTitle } from "@/components/ui/stepper";
const steps = [
{ title: "Fuel", description: "Load coal into slot 16." },
{ title: "Route", description: "Set the mining depth." },
{ title: "Deploy", description: "Send the turtle out." },
];
export default function StepperHorizontal() {
const [current, setCurrent] = React.useState(1);
const done = current >= steps.length;
return (
<div className="grid w-full max-w-2xl gap-6">
<Stepper orientation="horizontal" aria-label="Deploy a mining turtle">
{steps.map((step, i) => (
<StepperItem key={step.title} state={i < current ? "complete" : i === current ? "current" : "upcoming"}>
<StepperTitle>{step.title}</StepperTitle>
<StepperDescription>{step.description}</StepperDescription>
</StepperItem>
))}
</Stepper>
<div className="flex items-center justify-between gap-3 border-t pt-4">
<span aria-live="polite" className="font-mono text-[10px] tracking-[0.2em] text-muted-foreground uppercase">
{done ? "All steps done" : `Step ${current + 1} of ${steps.length}`}
</span>
<div className="flex gap-2">
<Button variant="outline" size="sm" disabled={current === 0} onClick={() => setCurrent((c) => c - 1)}>
<ArrowLeftIcon aria-hidden="true" /> Back
</Button>
<Button size="sm" disabled={done} onClick={() => setCurrent((c) => c + 1)}>
{current === steps.length - 1 ? "Finish" : "Next"} <ArrowRightIcon aria-hidden="true" />
</Button>
</div>
</div>
</div>
);
}