docs / patterns / step-chips

StepChips

a short numbered sequence in one row, sized to sit inside a banner or under a heading rather than to carry a page.

intensity · structuresrsc · server

provenance

shaped from sisu-plus

layer

core / patterns

intensity

structures · organizes a region, stays out of the content

composition

inside a CtaBanner or under a section heading, as a one-line pitchthree or four steps of a few words eachno interactive steps. it describes a sequence, it does not track oneno bodies under the steps, that is StepList

a11y

an ordered list, so position comes free · the number and arrow are aria-hidden to avoid double announcing · takes aria-labeljest-axe
live demo · try it out
  1. Install the extension
  2. Sign in
  3. Done
usagecopy
import { StepChips } from "usva/patterns/step-chips";

<StepChips
  aria-label="Setup steps"
  steps={["Install the extension", "Sign in", "Done"]}
/>

props

proptypedefaultnotes
stepsReact.ReactNode[]the steps, in order. keep each one a few words.
aria-labelstringnames the sequence, e.g. "Setup steps".

get it

npx shadcn add https://usva.build/r/step-chips.jsoncopy
source · components/ui/step-chips.tsxexactly what this command copiescopy
import * as React from "react";
import { cn } from "@/lib/utils";

export interface StepChipsProps
  extends Omit<React.HTMLAttributes<HTMLOListElement>, "children"> {
  steps: React.ReactNode[];
}

/**
 * A numbered sequence of chips, joined by arrows. The arrow lives inside its step
 * rather than between steps: an `ol` may only contain `li`, and sisu's sibling
 * fragments put the arrows outside the list entirely.
 */
export const StepChips = React.forwardRef<HTMLOListElement, StepChipsProps>(
  ({ className, steps, ...props }, ref) => {
    const last = steps.length - 1;
    return (
      <ol
        ref={ref}
        className={cn(
          "flex list-none flex-wrap items-center gap-1.5",
          className,
        )}
        {...props}
      >
        {steps.map((step, index) => (
          <li
            key={typeof step === "string" ? step : index}
            className="inline-flex items-center gap-1.5"
          >
            <span className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-ink/[0.04] px-2.5 py-1.5 text-xs text-muted">
              <span
                aria-hidden="true"
                className="grid size-[1.1rem] shrink-0 place-items-center rounded-full bg-accent-alt/15 text-[0.62rem] font-bold tabular-nums text-accent-alt"
              >
                {index + 1}
              </span>
              {step}
            </span>
            {index < last && (
              <span
                data-step-separator=""
                aria-hidden="true"
                className="hidden text-muted sm:inline"
              >
                &rarr;
              </span>
            )}
          </li>
        ))}
      </ol>
    );
  },
);
StepChips.displayName = "StepChips";