ATOM
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.

Preview

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>
ClasePropósito
tabs-stepsroot
tabs-steps__indexelement
tabs-steps__itemelement
tabs-steps__panelelement
tabs-steps__progresselement
tabs-steps__textelement
tabs-steps__titleelement
tabs-steps__trackelement
tabs-steps__triggerelement
tabs-steps__visualelement

Tokens resueltos

Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.

VariantePropValorToken
allbgnone(unparsed)
allfg#525252muted.foreground
allborder0(unparsed)

Animaciones

PropiedadDuraciónEasing
colorvar(--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;
  }
}

On this page

Detalles

Publicado14 de mayo de 2026
Categoriasurfaces
Lectura...
Visitas...
Ayuda?Slack

Componente

TabsSteps

Source

components-react / cssDisponible via MCP: atom_uikit_source("tabs-steps")