Styling

Style the headless primitives with the data attributes the API exposes.

Styling

Stepperize primitives ship no styles. Instead, each primitive exposes the current state as data-* attributes, and you style against them — with Tailwind variants, plain CSS, or any approach that can target an attribute selector. Your markup stays yours; the library just tells you what state each element is in.

The three attributes

These are the only attributes you need to know. Everything below builds on them.

AttributeValuesSet on
data-statusactive · previous · upcomingItem, Trigger, Indicator
data-orientationhorizontal · verticalRoot, List, Separator
data-componentstepper · stepper-list · stepper-item · stepper-trigger · stepper-contentevery primitive

data-status — the workhorse

data-status is the positional state of a step relative to the active one:

  • active — the current step.
  • previous — a step before the current one (already passed).
  • upcoming — a step after the current one (not reached yet).

With Tailwind

Target it with the data-[…] variant. A typical step indicator:

<Stepper.Indicator
  className="
    grid size-8 place-items-center rounded-full border text-sm font-medium
    data-[status=active]:border-primary data-[status=active]:bg-primary data-[status=active]:text-primary-foreground
    data-[status=previous]:border-primary data-[status=previous]:bg-primary/10 data-[status=previous]:text-primary
    data-[status=upcoming]:border-border data-[status=upcoming]:text-muted-foreground
  "
/>

Styling children with group-data

Put group on the element that has data-status, then read it from a child with group-data-[status=…]. This is how the blocks swap a number for a checkmark once a step is passed, without any JavaScript:

<Stepper.Indicator className="group …">
  {/* number while active/upcoming, checkmark once previous */}
  <span className="group-data-[status=previous]:hidden">{index + 1}</span>
  <Check className="hidden size-4 group-data-[status=previous]:block" />
</Stepper.Indicator>

When several elements on the same page each have their own data-status, name the group (group/itemgroup-data-[status=active]/item:) so a child reads the right one.

With plain CSS

No Tailwind required — attribute selectors work anywhere:

[data-component="stepper-indicator"][data-status="active"] {
  border-color: var(--primary);
  background: var(--primary);
}
[data-component="stepper-item"][data-status="upcoming"] {
  opacity: 0.6;
}

Here it is on a real block — the bar fills as data-status advances:

Your detailsStep 1 of 4
Your details” fields go here.

data-orientation — responsive layouts

orientation on Stepper.Root is reflected as data-orientation on the root, list, and separators (and drives keyboard behavior + aria-orientation). Style the two layouts from one definition:

<Stepper.List
  orientation="vertical"
  className="
    flex gap-2
    data-[orientation=horizontal]:flex-row
    data-[orientation=vertical]:flex-col
  "
/>

data-component — stable hooks for global CSS

Every primitive carries a data-component value, which gives you a stable selector that survives className changes — handy for design-system overrides or targeting nested primitives:

[data-component="stepper-list"] { gap: 0.5rem; }
[data-component="stepper-content"] { padding-block: 1rem; }

Status is positional, not completion

data-status is derived from the active index — it has no idea whether a step was actually finished. A "previous" step is simply behind the cursor, even if the user skipped its work. For business completion (a green ✓ that means done), use stepper.setComplete() / stepper.isComplete() and render your own marker. See Status vs. completion.

Where to go next

  • Primitives — the components that emit these attributes.
  • Blocks gallery — copy-paste components that put all of this together.
Edit on GitHub

Last updated on

On this page