Stepper
Numbered steps show progress through a sequence. Checkouts and multi-stage onboarding.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Checkout progress:
```tsx import { Stepper } from '@/components/atoms/Stepper';
<Stepper orientation="horizontal" steps={[ { state: 'completed', title: 'Cart', description: 'Items saved' }, { state: 'active', title: 'Payment' }, { state: 'upcoming', title: 'Confirm' }, ]} /> ```
## Accesibilidad
- Root is `role="list"` with `aria-label="Progress steps"`; the active step sets `aria-current="step"`. - Titles must be unique and human-readable — numbers alone are not enough.
## Cuándo no usar
- Single-page forms with no sequence — use headings / sections, not a stepper. - Tabs that switch content panels → `Tabs` (not a progress metaphor).
Uso
import { Stepper } from '@/components/atoms/Stepper';
<Stepper
orientation="horizontal"
steps={[
{ state: 'completed', title: 'Account' },
{ state: 'active', title: 'Plan' },
{ state: 'upcoming', title: 'Pay' },
]}
/>Props
state: 'completed' | 'active' | 'upcoming'title: string (required)description: string
Gotchas
- react
Stepper accepts orientation (horizontal|vertical) and steps: { state, title, description }[]. Keep exactly one step with state active.
- a11y
Titles must be unique and human-readable; do not rely on color alone to convey completed vs upcoming.
Anatomía CSS
<div class="stepper"> <span class="stepper__connector"></span> <span class="stepper__connector--completed"></span> <span class="stepper__content"></span> <span class="stepper__description"></span> <span class="stepper__indicator"></span> <span class="stepper__step"></span> <span class="stepper__step--active"></span> <span class="stepper__step--completed"></span> <span class="stepper__step--upcoming"></span> <span class="stepper__title"></span> </div>
| Clase | Propósito |
|---|---|
stepper | root |
stepper--horizontal | modifier |
stepper--vertical | modifier |
stepper__connector | element |
stepper__connector--completed | element |
stepper__content | element |
stepper__description | element |
stepper__indicator | element |
stepper__step | element |
stepper__step--active | element |
stepper__step--completed | element |
stepper__step--upcoming | element |
stepper__title | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #0a0a0a | primary |
| all | fg | #525252 | muted.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
background-color | var(--duration-200) | var(--easing-in-out) |
color | var(--duration-200) | var(--easing-in-out) |
background-color | var(--duration-200) | var(--easing-in-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* ============================================================
Stepper — numbered steps with states (completed, active, upcoming)
Connector is a separate flex item between steps, not absolute.
============================================================ */
.stepper {
display: flex;
align-items: flex-start;
}
.stepper--horizontal {
flex-direction: row;
}
.stepper--vertical {
flex-direction: column;
}
/* ---- Step item ---- */
.stepper__step {
display: flex;
align-items: flex-start;
gap: 12px;
flex-shrink: 0;
}
/* ---- Step indicator (number or checkmark) ---- */
.stepper__indicator {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
flex-shrink: 0;
border-radius: 9999px;
font-size: 12.8px;
font-weight: 600;
line-height: 1;
transition:
background-color 200ms cubic-bezier(0.4, 0, 0.2, 1),
color 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.stepper__step--upcoming .stepper__indicator {
background-color: #f5f5f5;
color: #525252;
}
.stepper__step--active .stepper__indicator {
background-color: #0a0a0a;
color: #fafafa;
}
.stepper__step--completed .stepper__indicator {
background-color: #0a0a0a;
color: #fafafa;
}
/* ---- Step content ---- */
.stepper__content {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 4px;
min-width: 0;
}
.stepper__title {
font-size: 12.8px;
font-weight: 500;
line-height: 1.3;
color: #0a0a0a;
}
.stepper__step--upcoming .stepper__title {
color: #525252;
}
.stepper__description {
font-size: 10.24px;
line-height: 1.4;
color: #525252;
}
/* ---- Connector (flex item between steps) ---- */
.stepper__connector {
background-color: #e5e5e5;
flex-shrink: 0;
transition: background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.stepper__connector--completed {
background-color: #0a0a0a;
}
/* Horizontal: line between steps, vertically centered on indicator */
.stepper--horizontal .stepper__connector {
align-self: flex-start;
margin-top: 1rem; /* center with 2rem indicator */
height: 1px;
width: 32px;
transform: translateY(-0.5px);
}
/* Vertical: line between steps, horizontally centered on indicator */
.stepper--vertical .stepper__connector {
margin-left: 1rem; /* center with 2rem indicator */
width: 1px;
height: 24px;
transform: translateX(-0.5px);
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.stepper__indicator,
.stepper__connector {
transition-duration: 0ms;
}
}
Codigo fuente
import { forwardRef, Fragment } from 'react'; export type StepState = 'completed' | 'active' | 'upcoming'; export type StepProps = { state?: StepState; title: string; description?: string; }; export type StepperProps = { orientation?: 'horizontal' | 'vertical'; steps: StepProps[]; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } function CheckIcon() { return ( <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" > <polyline points="20 6 9 17 4 12" /> </svg> ); } export const Stepper = forwardRef<HTMLDivElement, StepperProps>( ({ orientation = 'horizontal', steps, className }, ref) => { return ( <div ref={ref} className={cn('stepper', `stepper--${orientation}`, className)} role="list" aria-label="Progress steps" > {steps.map((step, index) => { const state = step.state ?? 'upcoming'; const isLast = index === steps.length - 1; return ( <Fragment key={`step-${index}`}> <div className={cn('stepper__step', `stepper__step--${state}`)} role="listitem" aria-current={state === 'active' ? 'step' : undefined} > <div className="stepper__indicator" aria-hidden="true"> {state === 'completed' ? <CheckIcon /> : index + 1} </div> <div className="stepper__content"> <span className="stepper__title">{step.title}</span> {step.description && ( <span className="stepper__description">{step.description}</span> )} </div> </div> {!isLast && ( <div className={cn( 'stepper__connector', state === 'completed' && 'stepper__connector--completed', )} aria-hidden="true" /> )} </Fragment> ); })} </div> ); }, ); Stepper.displayName = 'Stepper';
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Stepper) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.stepper__step--upcoming .stepper__indicator— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stepper__step--active .stepper__indicator— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stepper__step--completed .stepper__indicator— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stepper__step--upcoming .stepper__title— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stepper--horizontal .stepper__connector— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stepper--vertical .stepper__connector— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)@mediaon(prefers-reduced-motion: reduce)— not a Designer breakpoint — moved to head Custom Code block
Tras pegar: Create component → nombre Atom / Stepper → Publish.