Progress Steps
A horizontal step indicator: the connecting line fills as you advance and finished steps turn into ticks.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/progress-steps.json'use client'
import './progress-steps.css'
export interface ProgressStepsProps {
steps: string[]
current: number
onStepClick?: (i: number) => void
showLabels?: boolean
fill?: 'dark' | 'light'
className?: string
}
/** Horizontal step indicator with a filling track. */
export function ProgressSteps({ steps, current, onStepClick, showLabels = true, fill = 'dark', className }: ProgressStepsProps) {
return (
<ol className={['sd-pst', 'sd-pst__list', className].filter(Boolean).join(' ')} data-fill={fill}>
{steps.map((s, i) => {
const state = i < current ? 'done' : i === current ? 'active' : 'todo'
return (
<li key={s} className="sd-pst__item" data-state={state} aria-current={state === 'active' ? 'step' : undefined}>
{state === 'done' ? (
<button type="button" className="sd-pst__dot" aria-label={`Step ${i + 1}, ${s}, completed`} onClick={() => onStepClick?.(i)}><svg viewBox="0 0 16 16" aria-hidden><path d="m3.5 8.5 3 3 6-7" /></svg></button>
) : (
<span className="sd-pst__dot" aria-label={`Step ${i + 1}, ${s}`}>{i + 1}</span>
)}
{showLabels && <span className="sd-pst__lab" aria-hidden>{s}</span>}
</li>
)
})}
</ol>
)
}.sd-pst { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-pst[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-pst button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-pst :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-pst .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-pst { width: min(100%, 330px); display: grid; gap: 22px; }
.sd-pst__list.sd-pst { display: flex; gap: 0; }
.sd-pst__list { display: flex; list-style: none; margin: 0; padding: 0; }
.sd-pst__item { position: relative; flex: 1; display: grid; justify-items: center; gap: 8px; }
.sd-pst__item + .sd-pst__item::before, .sd-pst__item + .sd-pst__item::after { content: ''; position: absolute; top: 15px; right: calc(50% + 20px); left: calc(-50% + 20px); height: 2px; border-radius: 2px; background: var(--soft); }
.sd-pst__item + .sd-pst__item::after { background: var(--fg); transform-origin: left; transform: scaleX(0); transition: transform 360ms cubic-bezier(0.4, 0, 0.2, 1); }
.sd-pst__item[data-state='done']::after, .sd-pst__item[data-state='active']::after { transform: scaleX(1) !important; }
.sd-pst__dot { position: relative; z-index: 1; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 99px; font: 600 13px/1 ui-sans-serif, system-ui; color: var(--mute); background: var(--bg) !important; box-shadow: inset 0 0 0 1.5px var(--line); transition: background-color 250ms 200ms, color 250ms 200ms, box-shadow 250ms 200ms; }
.sd-pst__item[data-state='active'] .sd-pst__dot { color: var(--fg); box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-pst__item[data-state='done'] .sd-pst__dot { background: var(--strong) !important; color: var(--on); box-shadow: none; }
.sd-pst__dot svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; animation: sd-pst-pop 360ms 200ms cubic-bezier(0.3, 1.7, 0.4, 1) both; }
@keyframes sd-pst-pop { from { transform: scale(0); } }
.sd-pst__lab { font-size: 11.5px; color: var(--mute); text-align: center; }
.sd-pst__item[data-state='active'] .sd-pst__lab { color: var(--fg); font-weight: 600; }
.sd-pst__nav { display: flex; justify-content: space-between; }
.sd-pst__btn { height: 36px; padding: 0 16px; border-radius: 10px; font-weight: 500; font-size: 13.5px; box-shadow: inset 0 0 0 1px var(--line); }
.sd-pst__btn--next { background: var(--strong) !important; color: var(--on) !important; box-shadow: none; }
.sd-pst__btn:disabled { opacity: 0.35; cursor: default; }
@media (prefers-reduced-motion: reduce) { .sd-pst *, .sd-pst *::before, .sd-pst *::after { animation: none !important; transition: none !important; } }Overview
Numbered dots joined by a track. Moving forward fills the track to the next dot, then the finished dot flips to a tick; going back drains it.
Completed steps are clickable so people can jump back, but not ahead.
When to use
- Checkout, sign-up and setup wizards with 3–5 steps.
When not to use
- Long or branching flows: use a checklist.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/progress-steps.json$
npx shadcn@latest add https://sundaeui.dev/r/progress-steps.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/progress-steps.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/progress-steps.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy progress-steps.tsx and progress-steps.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { ProgressSteps } from '@/components/sundae/progress-steps'
<ProgressSteps />Props
| Prop | Type | Default |
|---|---|---|
steps*Step labels. | string[] | — |
current*Active step index (0-based). | number | — |
onStepClickCalled when a finished step is clicked. | (i: number) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- An ordered list; the active step has aria-current="step".
- Finished steps are buttons with names like "Step 1, Account, completed".
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-pst; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Progress Steps free?
- Yes. Progress Steps is free for personal and commercial projects, with full source on this page.
- How do I install Progress Steps?
- Run npx shadcn@latest add https://sundaeui.dev/r/progress-steps.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Progress Steps support keyboard and screen readers?
- An ordered list; the active step has aria-current="step". Finished steps are buttons with names like "Step 1, Account, completed".
- Does Progress Steps respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All navigation →- Pro
Command Palette
Navigation · Refined - Pro
Onboarding Flow
Forms · Refined Segmented Control
Navigation · RefinedPagination
Navigation · RefinedToolbar Tooltips
Navigation · RefinedUnderline Tabs
Navigation · Refined