Skip to content
txbrendan/ui

Step Dots

beta

A row of dots that shows progress through steps, like an onboarding flow, with each step named for screen readers.

Preview

neutral

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

brand

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

danger

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

success

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

warning

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

first

  1. Step 1 of 5: Account, current
  2. Step 2 of 5: Profile, upcoming
  3. Step 3 of 5: Team, upcoming
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

last

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, done
  4. Step 4 of 5: Billing, done
  5. Step 5 of 5: Invite, current

all done

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, done
  4. Step 4 of 5: Billing, done
  5. Step 5 of 5: Invite, done

back

  1. Step 5 of 5: Invite, upcoming

Click a done step to go back to it.

any

Click any step.

size

  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming
  1. Step 1 of 5: Account, done
  2. Step 2 of 5: Profile, done
  3. Step 3 of 5: Team, current
  4. Step 4 of 5: Billing, upcoming
  5. Step 5 of 5: Invite, upcoming

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/step-dots

Use it for

  • Showing where someone is in a short flow with a few steps: onboarding, a setup wizard, a checkout.
  • Pass one name per step. Sighted people see dots, and screen readers read each name, its place, and whether it is done.
  • Set jump='back' to let people click a done step to go back to it. Set jump='any' when every step can be done in any order.
  • Set current to steps.length when the flow is finished, so every dot shows as done.
  • For another language, pass aria-label, stepText, and stateText.

Avoid it for

  • Flows where people need to read the step names. Dots show no text, so use a list or tabs with visible labels.
  • More than about eight steps. The dots get hard to count. Show 'Step 3 of 12' as text instead.
  • Paging through a carousel of slides. Dots here mean progress, and done steps fill in.
  • Moving focus for the app. A jump only changes the current step. Your app shows that step and moves focus to it.

Props

  • steps

    One name per step, in order. Screen readers read each name with 'Step 2 of 4' and its state.

    readonly string[]

  • current

    The current step, counted from 0. Steps before it are done. Set it to steps.length when all are done. Use it with onCurrentChange.

    number

  • defaultCurrent

    The first current step when you do not control it.

    number · default 0

  • onCurrentChange

    Called with the step someone clicked. Clicking the current step does not call it.

    (index: number) => void

  • jump

    Which dots are buttons. back: done steps and the current one. any: every step. none: no buttons.

    "none" | "back" | "any" · default "none"

  • tone

    Color of the done and current dots.

    "neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"

  • aria-label

    Names the list. Pass a better name, like 'Setup progress'. With aria-labelledby set, the default is dropped.

    string · default "Progress"

  • stepText

    What screen readers hear for each step. {step} is its place, {total} the number of steps, {name} its name, and {state} a word from stateText.

    string · default "Step {step} of {total}: {name}, {state}"

  • stateText

    The words for {state} in stepText.

    { done: string; current: string; upcoming: string } · default { done: "done", current: "current", upcoming: "upcoming" }

  • className

    Extra classes for the list. Later classes win, so className='[--step-dots-size:6px]' makes the dots smaller.

    string

Accessibility

  • Renders an <ol>, so screen readers say how many steps there are.
  • Each step has hidden text, like 'Step 2 of 4: Profile, current'. stepText and stateText change the words. The dots themselves are hidden from screen readers.
  • The current step has aria-current="step".
  • With jump on, a clickable step is a native <button>, so Tab, Enter, and Space work. Steps people cannot reach are not buttons.
  • The current step stays a button while jump is on, so focus stays on the dot someone pressed when it becomes current.
  • Each dot sits in a cell at least 24px square, the smallest click target WCAG 2.2 allows.
  • The current dot is a wider pill, so it is not marked by color alone. Done dots use the tone fill and upcoming dots use --input. The theme tests hold both to 3:1 on the page and on cards.
  • The pill grows and shrinks over --motion-duration-base. data-motion="off" makes it instant. Reduced motion keeps the color change and drops the change of width.

Examples

Onboarding progress

<StepDots
  aria-label="Setup progress"
  steps={["Account", "Profile", "Team", "Invite"]}
  current={1}
/>

Go back to a done step

"use client";
import { useState } from "react";

const steps = ["Cart", "Shipping", "Payment", "Review"];

function CheckoutSteps() {
  const [step, setStep] = useState(2);
  return (
    <StepDots
      aria-label="Checkout progress"
      steps={steps}
      current={step}
      onCurrentChange={setStep}
      jump="back"
      tone="brand"
    />
  );
}

Every step done

const steps = ["Account", "Profile", "Team"];

<StepDots aria-label="Setup progress" steps={steps} current={steps.length} tone="success" />

In another language

<StepDots
  aria-label="Fortschritt der Einrichtung"
  steps={["Konto", "Profil", "Team", "Einladen"]}
  current={1}
  stepText="Schritt {step} von {total}: {name}, {state}"
  stateText={{ done: "erledigt", current: "aktuell", upcoming: "offen" }}
/>