ProgressNav
A stepped rail that fills as you advance through a flow. Wizards and multi-step forms.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Anchored section rail:
```tsx import { ProgressNav } from '@/components/atoms/ProgressNav';
<ProgressNav items={[ { id: 'features', label: 'Features' }, { id: 'pricing', label: 'Pricing' }, ]} cta={{ label: 'Contact', href: '#contact' }} /> ```
## Accesibilidad
- Root is a `nav`; keep labels short. Section `id`s on the page must match `items[].id`. - Motion hooks read `data-progress-nav-*` — without them the fill indicator will not track scroll.
## Cuándo no usar
- Discrete checkout steps with state → `Stepper`. - In-panel tab switching without page sections → `Tabs`.
Uso
import { ProgressNav } from '@/components/atoms/ProgressNav';
<ProgressNav
items={[{ id: 'features', label: 'Features' }, { id: 'pricing', label: 'Pricing' }]}
cta={{ label: 'Contact', href: '#contact' }}
/>Props
items: ProgressNavItem[] (required)logo: ReactNodecta: { label: stringhref: string (required)
Gotchas
- react
items is required [{id,label}]. Anchors must match section ids on the page. Motion hooks read data-progress-nav-* attributes.
- a11y
Root is a nav; keep labels short. Logo links to topAnchor (default top); CTA is a plain href.
Anatomía CSS
<div class="progress-nav"> <span class="progress-nav__btn"></span> <span class="progress-nav__btn-text"></span> <span class="progress-nav__contact-btn"></span> <span class="progress-nav__indicator"></span> <span class="progress-nav__inner"></span> <span class="progress-nav__list"></span> <span class="progress-nav__logo"></span> <span class="progress-nav__logo-svg"></span> <span class="progress-nav__wrapper"></span> </div>
| Clase | Propósito |
|---|---|
progress-nav | root |
progress-nav__btn | element |
progress-nav__btn-text | element |
progress-nav__contact-btn | element |
progress-nav__indicator | element |
progress-nav__inner | element |
progress-nav__list | element |
progress-nav__logo | element |
progress-nav__logo-svg | element |
progress-nav__wrapper | 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 | fg | #fafafa | primary.foreground |
| all | bg | #0a0a0a | primary |
| all | hover-bg | #262626 | primary.hover |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
all | var(--duration-700) | cubic-bezier(0.16 |
1 | | |
0.3 | | |
1) | | |
transform | var(--duration-500) | cubic-bezier(0.16 |
1 | | |
0.3 | | |
1) | | |
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).
/* ============================================================
ProgressNav — fixed one-page navigation with scroll indicator
Uses GSAP ScrollTrigger via @atom-uikit/animations
============================================================ */
.progress-nav {
width: 100%;
padding: 24px;
position: fixed;
top: 0;
left: 0;
z-index: 30;
}
.progress-nav__inner {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
/* ---- Logo ---- */
.progress-nav__logo {
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
}
.progress-nav__logo-svg {
width: 8em;
height: auto;
}
/* ---- Pill wrapper (contains the nav list) ---- */
.progress-nav__wrapper {
background-color: #f5f5f5;
border-radius: 9999px;
padding: 4px;
}
/* ---- Nav list ---- */
.progress-nav__list {
border-radius: 9999px;
display: flex;
justify-content: flex-start;
align-items: center;
position: relative;
overflow: hidden;
}
/* ---- Sliding indicator ---- */
.progress-nav__indicator {
z-index: 2;
background-color: #ffffff;
border-radius: 9999px;
width: 2.5em;
height: 2.5em;
position: absolute;
left: -2.5em;
transition: all 700ms cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
/* ---- Nav buttons ---- */
.progress-nav__btn {
z-index: 3;
cursor: pointer;
color: inherit;
display: flex;
justify-content: center;
align-items: center;
height: 2.5em;
padding-left: 1em;
padding-right: 1em;
text-decoration: none;
position: relative;
overflow: hidden;
}
/* Hidden sentinel buttons (before/after) */
.progress-nav__btn.is--before {
z-index: 1;
width: 2.5em;
height: 2.5em;
padding: 0;
position: absolute;
right: 100%;
}
.progress-nav__btn.is--after {
z-index: 1;
width: 2.5em;
height: 2.5em;
padding: 0;
position: absolute;
left: 100%;
}
/* ---- Text with hover slide animation ---- */
.progress-nav__btn-text {
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 16px;
font-weight: 500;
transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
transform: translateY(0%) rotate(0.001deg);
}
.progress-nav__btn-text.is--duplicate {
position: absolute;
top: 100%;
}
.progress-nav__btn:hover .progress-nav__btn-text,
.progress-nav__contact-btn:hover .progress-nav__btn-text {
transform: translateY(-100%) rotate(0.001deg);
}
/* ---- CTA button ---- */
.progress-nav__contact-btn {
color: #fafafa;
background-color: #0a0a0a;
border-radius: 9999px;
height: 3.5em;
padding-left: 24px;
padding-right: 24px;
text-decoration: none;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
transition: background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.progress-nav__contact-btn:hover {
background-color: #262626;
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.progress-nav__indicator {
transition-duration: 0ms;
}
.progress-nav__btn-text {
transition-duration: 0ms;
}
.progress-nav__contact-btn {
transition-duration: 0ms;
}
}
Codigo fuente
import { forwardRef, type ReactNode } from 'react'; export type ProgressNavItem = { id: string; label: string; }; export type ProgressNavProps = { items: ProgressNavItem[]; logo?: ReactNode; cta?: { label: string; href: string; }; topAnchor?: string; bottomAnchor?: string; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } function NavButton({ id, label }: { id: string; label: string }) { return ( <a data-progress-nav-target={`#${id}`} href={`#${id}`} className="progress-nav__btn" > <span className="progress-nav__btn-text">{label}</span> <span className="progress-nav__btn-text is--duplicate">{label}</span> </a> ); } export const ProgressNav = forwardRef<HTMLElement, ProgressNavProps>( ({ items, logo, cta, topAnchor = 'top', bottomAnchor = 'bottom', className }, ref) => { return ( <nav ref={ref} className={cn('progress-nav', className)}> <div className="progress-nav__inner"> {logo && ( <a href={`#${topAnchor}`} className="progress-nav__logo"> {logo} </a> )} <div className="progress-nav__wrapper"> <div data-progress-nav-list="" className="progress-nav__list"> <div className="progress-nav__indicator" /> <div data-progress-nav-target={`#${topAnchor}`} className="progress-nav__btn is--before" /> {items.map((item) => ( <NavButton key={item.id} id={item.id} label={item.label} /> ))} <div data-progress-nav-target={`#${bottomAnchor}`} className="progress-nav__btn is--after" /> </div> </div> {cta && ( <a href={cta.href} className="progress-nav__contact-btn"> <span className="progress-nav__btn-text">{cta.label}</span> <span className="progress-nav__btn-text is--duplicate">{cta.label}</span> </a> )} </div> </nav> ); }, ); ProgressNav.displayName = 'ProgressNav';