Vertical, non-linear install checklist with a shadcn CLI command in a monospace block under each open step. Done, next ticks the step and moves focus to the following title.
"use client"
import { useEffect, useId, useRef, useState } from "react"
import { CheckIcon } from "@phosphor-icons/react"
import { cn } from "cn"
import {
Stepper,
StepperContent,
StepperDescription,
StepperIndicator,
StepperItem,
StepperList,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@/components/stepper/stepper"
import { Button } from "@/components/ui/button"
// The copy: change text here, not in the markup.
const COPY = {
title: "Install the registry",
body: "Three commands. Mark each one done as you go.",
done: "Done, next",
finished: "All set. Browse the registry to add more.",
steps: [
{
title: "Initialise shadcn",
body: "Creates components.json and the ui folder.",
cmd: "pnpm dlx shadcn@latest init",
},
{
title: "Add the registry",
body: "Points the CLI at Orbcore.",
cmd: "pnpm dlx shadcn@latest registry add @01orbital",
},
{
title: "Install a block",
body: "Any block, one command.",
cmd: "pnpm dlx shadcn@latest add @01orbital/checkout",
},
],
} as const
// Vertical with the command inline under the open step. Non-linear, so a
// reader can jump back to copy a command again.
export default function InstallGuide() {
const [step, setStep] = useState(0)
// Done is its own set, so jumping back to copy a command does not undo it.
const [done, setDone] = useState<number[]>([])
const id = useId()
const focusNext = useRef(false)
// Done, next unmounts the button under the pointer; focus lands on the
// next step's title instead of falling to the body.
useEffect(() => {
if (!focusNext.current) return
focusNext.current = false
;(
document.getElementById(`${id}step-${step}`) ??
document.getElementById(`${id}finished`)
)?.focus()
}, [id, step])
const finish = (i: number) => {
const nextDone = done.includes(i) ? done : [...done, i]
setDone(nextDone)
const nextStep = COPY.steps.findIndex(
(_, index) => !nextDone.includes(index)
)
focusNext.current = true
setStep(nextStep === -1 ? COPY.steps.length : nextStep)
}
const finished = done.length === COPY.steps.length
return (
<section className="mx-auto w-full max-w-md">
<h2 className="text-xl font-semibold tracking-tight">{COPY.title}</h2>
<p className="mt-1 mb-6 text-sm text-muted-foreground">{COPY.body}</p>
<Stepper value={step} onValueChange={setStep} linear={false}>
<StepperList className="flex-col items-stretch">
{COPY.steps.map((s, i) => (
<StepperItem
key={s.title}
value={i}
className="flex-col items-stretch"
>
<div className="flex gap-3">
<div className="flex flex-col items-center">
<StepperIndicator
className={cn(
"size-6 text-[11px]",
done.includes(i) &&
"bg-primary text-primary-foreground ring-0"
)}
>
{done.includes(i) ? (
<CheckIcon weight="bold" aria-hidden className="size-3" />
) : (
<span>{i + 1}</span>
)}
</StepperIndicator>
<StepperSeparator className="mx-0 my-1 h-auto w-px flex-1 group-last/step:hidden" />
</div>
<div className="min-w-0 flex-1 pt-0.5 pb-6">
<StepperTrigger
id={`${id}step-${i}`}
className="h-auto justify-start px-0 py-0 text-left hover:bg-transparent"
>
<StepperTitle>{s.title}</StepperTitle>
</StepperTrigger>
<StepperContent value={i} className="mt-1 gap-3">
<StepperDescription>{s.body}</StepperDescription>
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs leading-6">
{s.cmd}
</pre>
<Button
size="sm"
className="w-fit"
onClick={() => finish(i)}
>
{COPY.done}
</Button>
</StepperContent>
</div>
</div>
</StepperItem>
))}
</StepperList>
{finished && (
<p
id={`${id}finished`}
tabIndex={-1}
className="text-sm outline-none"
>
{COPY.finished}
</p>
)}
</Stepper>
</section>
)
}
pnpm dlx shadcn add @01orbital/install-guidenpx shadcn add @01orbital/install-guideyarn shadcn add @01orbital/install-guidebunx --bun shadcn add @01orbital/install-guideFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/blocks/stepper/install-guide/index.tsx, as source you own and edit.
import InstallGuide from "@/components/blocks/stepper/install-guide"
export default function Example() {
return <InstallGuide />
}npm: @phosphor-icons/react, cn. The CLI installs them for you.
Registry pieces it installs with it:
buttonfont-geist-monoImport Wizard Bars
Bars variant of Import Wizard: CSV upload with an in-browser row count, column mapping, a preview table and import confirmation, tracked by a segmented rail rather than numbered steps.
Orb Steps
Horizontal pill stepper for Account, Workspace and Invite team where each step's indicator is a Cloudy Nimbus disc: coral, lilac and moss. Done steps fill ink, the current step wears a ring, upcoming steps stay bare; the row stands alone, so your own form submit drives the step.