# Stepper with vertical status bars

> 5 incident resolution steps, each marked by a thin vertical bar that is gray, green when done or dashed blue while current, with the detail beside.

A shadcn/ui block from the [@reui](https://reui.io) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/reui/c-stepper-42).

- Page, with a live preview: https://shadcn.exchange/r/reui/c-stepper-42
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-stepper-42-light-895d90ca344046d7.png
- Registry item: https://reui.io/r/new-york-v4/c-stepper-42.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://reui.io/r/new-york-v4/c-stepper-42.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `button`, `separator`, `@reui/stepper`

## Files

### `c-stepper-42.tsx`

```tsx
"use client"

import { useRef, useState } from "react"
import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperNav,
  StepperPanel,
  StepperTitle,
  StepperTrigger,
} from "@/components/reui/stepper"

import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"

const steps = [
  {
    title: "Resolution Planning",
    detail: "Hotfix chosen over a rollback, with an owner per service.",
  },
  {
    title: "Scope Alignment",
    detail: "Checkout and payments are in scope, search waits a sprint.",
  },
  {
    title: "Implementation",
    detail: "Rolling the patch out to 6 pods behind a feature flag.",
  },
  {
    title: "Outcome Verification",
    detail: "Watching error rate and p95 latency for 30 minutes.",
  },
  {
    title: "Final Summary",
    detail: "Root cause, timeline and follow-ups for the postmortem.",
  },
]

// The step in progress splits its 40px bar into four 8px dashes.
const active = (
  <span className="flex h-10 flex-col justify-between">
    {[1, 2, 3, 4].map((dash) => (
      <span key={dash} className="h-2 w-0.5 rounded-full bg-blue-500" />
    ))}
  </span>
)

export function Pattern() {
  const [current, setCurrent] = useState(2)
  const prevRef = useRef<HTMLButtonElement>(null)
  const nextRef = useRef<HTMLButtonElement>(null)
  // One past the last step marks every step complete. No tab is selected
  // then, so the last one keeps the tablist's tab stop.
  const allDone = current > steps.length

  function goTo(step: number) {
    setCurrent(step)
    // The pressed button disables itself at either end, so focus moves to
    // its partner.
    if (step === 1) nextRef.current?.focus()
    if (step > steps.length) prevRef.current?.focus()
  }

  // Side by side from 28rem, which leaves 13rem beside the 13rem steps for
  // the text and the Previous and Next row. Below that both stack in the
  // same 13rem column.
  return (
    <Stepper
      value={current}
      onValueChange={setCurrent}
      orientation="vertical"
      indicators={{ active }}
      className="@container mx-auto max-w-lg"
      aria-label="Incident resolution"
    >
      <div className="mx-auto flex max-w-52 flex-col gap-6 @md:max-w-none @md:flex-row @md:items-start">
        <StepperNav className="w-full gap-3 @md:w-52 @md:shrink-0">
          {steps.map((item, index) => {
            const step = index + 1

            return (
              <StepperItem
                key={item.title}
                step={step}
                className="items-stretch"
              >
                {/* The whole row is the tab, while the focus ring moves onto
                    the label: a pill around the row would cut through the
                    bar. */}
                <StepperTrigger
                  tabIndex={allDone && step === steps.length ? 0 : undefined}
                  className="gap-1.5 focus-visible:ring-0"
                >
                  <StepperIndicator className="bg-input data-[state=completed]:bg-success h-10 w-0.5 data-[state=active]:bg-transparent" />

                  {/* 6px gap plus 10px padding make the drawn 16px, and the
                      padding keeps the ring's rounded ends clear of the
                      text. */}
                  <div className="ring-ring/50 flex min-w-0 flex-col justify-center self-stretch rounded-full px-2.5 text-start in-focus-visible:ring-3">
                    {/* truncate clips at the line box, so the title gets
                        text-sm's own 20px line back from leading-none, which
                        clips descenders. With the 16px caption: 36px of 40. */}
                    <StepperTitle className="data-[state=inactive]:text-muted-foreground truncate leading-5">
                      {item.title}
                    </StepperTitle>
                    <StepperDescription className="text-xs whitespace-nowrap">
                      Step {step}
                    </StepperDescription>
                    {/* Only the bar's color says a step is done, so screen
                        readers get it in words. */}
                    {step < current && (
                      <span className="sr-only">, completed</span>
                    )}
                  </div>
                </StepperTrigger>
              </StepperItem>
            )
          })}
        </StepperNav>

        {/* Decorative, so hidden from assistive tech: a line across while the
            columns stack, a line between them once they sit side by side. */}
        <Separator aria-hidden="true" className="@md:hidden" />
        <Separator
          aria-hidden="true"
          orientation="vertical"
          className="hidden @md:block"
        />
        <div className="flex min-w-0 flex-1 flex-col gap-3 text-start">
          <StepperPanel>
            {steps.map((item, index) => (
              <StepperContent key={item.title} value={index + 1}>
                <p className="text-muted-foreground text-xs">
                  Step {index + 1} of {steps.length}
                </p>
                <p className="text-sm font-medium">{item.title}</p>
                <p className="text-muted-foreground text-sm text-pretty">
                  {item.detail}
                </p>
              </StepperContent>
            ))}
            <StepperContent value={steps.length + 1}>
              <p className="text-muted-foreground text-xs">
                All {steps.length} steps done
              </p>
              <p className="text-sm font-medium">Incident resolved</p>
              <p className="text-muted-foreground text-sm text-pretty">
                The final summary is shared and the incident closed.
              </p>
            </StepperContent>
          </StepperPanel>
          <div className="flex justify-start gap-2">
            <Button
              ref={prevRef}
              variant="outline"
              size="sm"
              disabled={current === 1}
              onClick={() => goTo(current - 1)}
            >
              Previous
            </Button>
            <Button
              ref={nextRef}
              variant="outline"
              size="sm"
              disabled={allDone}
              onClick={() => goTo(current + 1)}
            >
              Next
            </Button>
          </div>
        </div>
      </div>
    </Stepper>
  )
}
```
