Components (registry)Surfaces
Tabs Steps
Numbered steps, one open at a time, each with its own visual. Flat structure: item and visual are siblings, so the visual falls under its step on mobile and moves to the right column on desktop without leaving the flow.
Anatomía CSS
<div class="tabs-steps"> <span class="tabs-steps__index"></span> <span class="tabs-steps__item"></span> <span class="tabs-steps__panel"></span> <span class="tabs-steps__progress"></span> <span class="tabs-steps__text"></span> <span class="tabs-steps__title"></span> <span class="tabs-steps__track"></span> <span class="tabs-steps__trigger"></span> <span class="tabs-steps__visual"></span> </div>
| Clase | Propósito |
|---|---|
tabs-steps | root |
tabs-steps__index | element |
tabs-steps__item | element |
tabs-steps__panel | element |
tabs-steps__progress | element |
tabs-steps__text | element |
tabs-steps__title | element |
tabs-steps__track | element |
tabs-steps__trigger | element |
tabs-steps__visual | 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 | none | (unparsed) |
| all | fg | #525252 | muted.foreground |
| all | border | 0 | (unparsed) |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
color | var(--duration-200) | var(--easing-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).
/* -------------------------------------------------------------------------
Tabs Steps
Pasos numerados, cada uno con su visual. Uno abierto a la vez, con avance
automatico y barra de progreso.
ARIA de ACORDEON y no de tablist a proposito: en movil el visual cae dentro
del paso, asi que lo que hay es un desplegable. role="tab" le prometeria a un
lector de pantalla navegacion por flechas entre pestañas que en ese tamaño no
existe, y el disparador es un <button>, no un <a href="#"> que salta al top.
ESTRUCTURA PLANA: item y visual son HERMANOS, no anidados.
.tabs-steps
.tabs-steps__item (1)
.tabs-steps__visual (1)
.tabs-steps__item (2)
.tabs-steps__visual (2)
Eso es lo que hace que en movil cada visual caiga bajo su paso sin una sola
regla especial, y que en desktop el grid los mande a la columna derecha sin
sacar nada del flujo.
La alternativa evaluada —visual dentro del panel, position:absolute en
desktop— rompia de dos maneras: la seccion dejaba de contar la altura del
visual, y cualquier ancestro con transform pasaba a ser su contenedor de
referencia. Este componente usa GSAP, asi que el segundo fallo no era
hipotetico: bastaba con animar un paso para que el visual saltara dentro de
el, sin error y sin aviso.
Parts: .tabs-steps, __item, __trigger, __index, __title, __panel, __text,
__track, __progress, __visual
------------------------------------------------------------------------- */
.tabs-steps {
display: grid;
gap: 40px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
}
/* ---- Paso ---- */
.tabs-steps__item {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding-block: 16px;
padding-inline-end: 24px;
}
/* La pista inferior marca el paso; la barra la rellena mientras corre el
autoplay. Van en dos elementos y no en un border porque un borde no se puede
escalar desde un extremo. */
.tabs-steps__track {
position: absolute;
inset-block-end: 0;
inset-inline: 0;
height: 2px;
background-color: #e5e5e5;
}
.tabs-steps__progress {
height: 100%;
background-color: #ff6600;
transform: scaleX(0);
transform-origin: left center;
}
/* ---- Disparador ---- */
.tabs-steps__trigger {
display: flex;
align-items: baseline;
gap: 12px;
width: 100%;
margin: 0;
padding: 0;
border: 0;
background: none;
color: #525252;
font: inherit;
text-align: start;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: color 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
.tabs-steps__trigger[aria-expanded='true'] {
color: #0a0a0a;
}
.tabs-steps__trigger:focus-visible {
outline: 2px solid #0a0a0a;
outline-offset: 1.5px;
}
.tabs-steps__index {
flex: none;
font-variant-numeric: tabular-nums;
}
.tabs-steps__title {
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 1.45;
letter-spacing: -0.02em;
}
/* ---- Panel ---- */
/* La altura la anima el behavior; overflow hidden es lo que hace que el texto
se recorte en vez de desbordar mientras colapsa. */
.tabs-steps__panel {
overflow: hidden;
}
.tabs-steps__text {
margin: 0;
padding-block-start: 12px;
color: #525252;
font-size: 16px;
line-height: 1.5;
text-wrap: pretty;
}
/* ---- Visual ---- */
.tabs-steps__visual {
overflow: hidden;
border-radius: 16px;
}
.tabs-steps__visual > * {
display: block;
width: 100%;
height: auto;
}
/* En movil el visual es un desplegable mas: cerrado no ocupa. No se puede
heredar del panel porque es su HERMANO, no su hijo — el behavior marca el
par. */
.tabs-steps__visual:not([data-active]) {
display: none;
}
/* ---- Desktop ---- */
@media (min-width: 992px) {
.tabs-steps {
grid-template-columns: 1fr 1fr;
/* Filas min-content MAS una pista 1fr al final. min-content solo no basta:
un item que abarca pistas intrinsecas reparte entre ellas lo que le falta,
asi que el visual (mas alto que los tres pasos juntos) seguia separando
los pasos ~97px entre si en la home (medido 2026-09-18). Con una pista
flexible dentro de su rango, la especificacion de grid le da a ella todo
el sobrante y los pasos quedan a su altura. */
grid-template-rows: repeat(3, min-content) 1fr;
column-gap: 40px;
/* Los pasos ya traen su propio padding-block; en escritorio el hueco entre
filas solo tiene que separarlos, no volver a espaciarlos. */
row-gap: 8px;
align-content: start;
}
.tabs-steps__item {
grid-column: 1;
}
/* Todos los visuales ocupan la MISMA celda de la columna derecha y se apilan;
solo el activo se pinta. Al abarcar todas las filas, la altura de la
seccion la resuelve el grid y ningun visual puede desbordarla. */
.tabs-steps__visual {
grid-column: 2;
grid-row: 1 / -1;
align-self: start;
justify-self: center;
overflow: hidden;
/* El tope va en ANCHO, no en alto.
El video es vertical (630x810): a media pantalla pedia mas de 1000px y
arrastraba con el la altura de toda la seccion, porque un item que abarca
filas las estira hasta caber. Acotando el ancho conserva su proporcion —
ni recortado ni con barras a los lados, que es lo que pasaria topando el
alto. 24rem de ancho son unos 31rem de alto, a la altura de los pasos. */
max-width: 24rem;
min-height: 0;
}
/* El canal de Webflow envuelve los embeds en un div propio: sin esto, ese
wrapper conserva la altura natural del video y el tope no llega al medio. */
.tabs-steps__visual > * {
min-height: 0;
}
.tabs-steps__visual video,
.tabs-steps__visual img {
display: block;
width: 100%;
height: auto;
}
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.tabs-steps__trigger {
transition-duration: 0ms;
}
/* Una barra que se llena sola anuncia un cambio automatico que en este modo
no ocurre: mentiria. */
.tabs-steps__track {
display: none;
}
}
Codigo fuente
styles/components/tabs-steps.css
/* ------------------------------------------------------------------------- Tabs Steps Pasos numerados, cada uno con su visual. Uno abierto a la vez, con avance automatico y barra de progreso. ARIA de ACORDEON y no de tablist a proposito: en movil el visual cae dentro del paso, asi que lo que hay es un desplegable. role="tab" le prometeria a un lector de pantalla navegacion por flechas entre pestañas que en ese tamaño no existe, y el disparador es un <button>, no un <a href="#"> que salta al top. ESTRUCTURA PLANA: item y visual son HERMANOS, no anidados. .tabs-steps .tabs-steps__item (1) .tabs-steps__visual (1) .tabs-steps__item (2) .tabs-steps__visual (2) Eso es lo que hace que en movil cada visual caiga bajo su paso sin una sola regla especial, y que en desktop el grid los mande a la columna derecha sin sacar nada del flujo. La alternativa evaluada —visual dentro del panel, position:absolute en desktop— rompia de dos maneras: la seccion dejaba de contar la altura del visual, y cualquier ancestro con transform pasaba a ser su contenedor de referencia. Este componente usa GSAP, asi que el segundo fallo no era hipotetico: bastaba con animar un paso para que el visual saltara dentro de el, sin error y sin aviso. Parts: .tabs-steps, __item, __trigger, __index, __title, __panel, __text, __track, __progress, __visual ------------------------------------------------------------------------- */ .tabs-steps { display: grid; gap: var(--spacing-10); font-family: var(--font-family-sans), ui-sans-serif, system-ui, sans-serif; } /* ---- Paso ---- */ .tabs-steps__item { position: relative; display: flex; flex-direction: column; gap: var(--spacing-1); padding-block: var(--spacing-4); padding-inline-end: var(--spacing-6); } /* La pista inferior marca el paso; la barra la rellena mientras corre el autoplay. Van en dos elementos y no en un border porque un borde no se puede escalar desde un extremo. */ .tabs-steps__track { position: absolute; inset-block-end: 0; inset-inline: 0; height: var(--stroke-medium); background-color: var(--border); } .tabs-steps__progress { height: 100%; background-color: var(--brand); transform: scaleX(0); transform-origin: left center; } /* ---- Disparador ---- */ .tabs-steps__trigger { display: flex; align-items: baseline; gap: var(--spacing-3); width: 100%; margin: 0; padding: 0; border: 0; background: none; color: var(--muted-foreground); font: inherit; text-align: start; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-200) var(--easing-out); } .tabs-steps__trigger[aria-expanded='true'] { color: var(--foreground); } .tabs-steps__trigger:focus-visible { outline: var(--ring-width) solid var(--ring); outline-offset: var(--ring-offset); } .tabs-steps__index { flex: none; font-variant-numeric: tabular-nums; } .tabs-steps__title { margin: 0; font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); line-height: var(--line-height-lg); letter-spacing: var(--letter-spacing-tight); } /* ---- Panel ---- */ /* La altura la anima el behavior; overflow hidden es lo que hace que el texto se recorte en vez de desbordar mientras colapsa. */ .tabs-steps__panel { overflow: hidden; } .tabs-steps__text { margin: 0; padding-block-start: var(--spacing-3); color: var(--muted-foreground); font-size: var(--font-size-base); line-height: var(--line-height-base); text-wrap: pretty; } /* ---- Visual ---- */ .tabs-steps__visual { overflow: hidden; border-radius: var(--radius-xl); } .tabs-steps__visual > * { display: block; width: 100%; height: auto; } /* En movil el visual es un desplegable mas: cerrado no ocupa. No se puede heredar del panel porque es su HERMANO, no su hijo — el behavior marca el par. */ .tabs-steps__visual:not([data-active]) { display: none; } /* ---- Desktop ---- */ @media (min-width: 992px) { .tabs-steps { grid-template-columns: 1fr 1fr; /* Filas min-content MAS una pista 1fr al final. min-content solo no basta: un item que abarca pistas intrinsecas reparte entre ellas lo que le falta, asi que el visual (mas alto que los tres pasos juntos) seguia separando los pasos ~97px entre si en la home (medido 2026-09-18). Con una pista flexible dentro de su rango, la especificacion de grid le da a ella todo el sobrante y los pasos quedan a su altura. */ grid-template-rows: repeat(var(--tabs-steps-count, 3), min-content) 1fr; column-gap: var(--spacing-10); /* Los pasos ya traen su propio padding-block; en escritorio el hueco entre filas solo tiene que separarlos, no volver a espaciarlos. */ row-gap: var(--spacing-2); align-content: start; } .tabs-steps__item { grid-column: 1; } /* Todos los visuales ocupan la MISMA celda de la columna derecha y se apilan; solo el activo se pinta. Al abarcar todas las filas, la altura de la seccion la resuelve el grid y ningun visual puede desbordarla. */ .tabs-steps__visual { grid-column: 2; grid-row: 1 / -1; align-self: start; justify-self: center; overflow: hidden; /* El tope va en ANCHO, no en alto. El video es vertical (630x810): a media pantalla pedia mas de 1000px y arrastraba con el la altura de toda la seccion, porque un item que abarca filas las estira hasta caber. Acotando el ancho conserva su proporcion — ni recortado ni con barras a los lados, que es lo que pasaria topando el alto. 24rem de ancho son unos 31rem de alto, a la altura de los pasos. */ max-width: var(--tabs-steps-visual-max, 24rem); min-height: 0; } /* El canal de Webflow envuelve los embeds en un div propio: sin esto, ese wrapper conserva la altura natural del video y el tope no llega al medio. */ .tabs-steps__visual > * { min-height: 0; } .tabs-steps__visual video, .tabs-steps__visual img { display: block; width: 100%; height: auto; } } /* ---- Reduced motion ---- */ @media (prefers-reduced-motion: reduce) { .tabs-steps__trigger { transition-duration: 0ms; } /* Una barra que se llena sola anuncia un cambio automatico que en este modo no ocurre: mentiria. */ .tabs-steps__track { display: none; } }