ATOM
Components (registry)Navigation

Table of Contents

A side rail that highlights the heading you are reading and shows depth. Long docs; links come from headings.

Preview

Anatomía CSS

<div class="toc">
  <span class="toc__label"></span>
  <span class="toc__link"></span>
  <span class="toc__list"></span>
</div>
ClasePropósito
tocroot
toc--collapsiblemodifier
toc__labelelement
toc__linkelement
toc__listelement

Tokens resueltos

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

VariantePropValorToken
allfg#0a0a0aforeground
allhover-fg#0a0a0aforeground
collapsiblefg#525252muted.foreground

Animaciones

PropiedadDuraciónEasing
colorvar(--duration-200)var(--easing-out)
border-colorvar(--duration-200)var(--easing-out)
rotatevar(--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).

/* -------------------------------------------------------------------------
   Table of Contents

   Indice de un documento largo: rail de links generados desde los headings del
   contenido. A diferencia de `progress-nav` (secciones declaradas a mano, con
   indicador deslizante), aqui los links NO se escriben: el behavior
   `table-of-contents` de packages/animations los clona desde un template y
   marca el activo al hacer scroll.

   Contrato de estado (lo escribe el behavior, no el consumidor):
     [data-toc-status="active"]  link de la seccion visible
     [data-toc-depth="2|3|4"]    nivel del heading, para la indentacion

   El scroll-spy es el behavior; este archivo solo pinta el estado.
   ------------------------------------------------------------------------- */

.toc {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.toc__label {
  color: #525252;
}

.toc__list {
  display: flex;
  flex-direction: column;
}

.toc__link {
  display: block;
  padding-block: 8px;
  padding-inline: 16px 8px;
  /* El rail es un borde por link, no un borde en la lista: asi el tramo activo
     se pinta sin que un pseudo-elemento tenga que medir posiciones. */
  border-inline-start: 1.5px solid #e5e5e5;
  color: #525252;
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif;
  font-size: 12.8px;
  line-height: 1.45;
  text-decoration: none;
  text-wrap: pretty;
  /* Un heading legal puede traer una palabra larguisima o una URL: en un rail de
     15rem tiene que partir, no sobresalir. */
  overflow-wrap: break-word;
  transition:
    color 200ms cubic-bezier(0.22, 1, 0.36, 1),
    border-color 200ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
  .toc__link:hover {
    color: #0a0a0a;
    border-inline-start-color: #525252;
  }
}

.toc__link[data-toc-status='active'] {
  color: #0a0a0a;
  font-weight: 500;
  /* Naranja de marca como acento fino: 1.5px de rail, jamas relleno ni texto. */
  border-inline-start-color: #ff6600;
}

.toc__link:focus-visible {
  outline: 2px solid #0a0a0a;
  outline-offset: 1.5px;
  border-radius: 4px;
}

/* ---- Profundidad ----
   Se indenta el padding, no el margen: el rail (borde) debe quedar alineado en
   todos los niveles, si no la linea se rompe en cada sub-seccion. */

.toc__link[data-toc-depth='3'] {
  padding-inline-start: calc(16px + 16px);
}

.toc__link[data-toc-depth='4'] {
  padding-inline-start: calc(16px + 32px);
}

/* ---- Colapsado (movil) ----
   En pantallas angostas el indice no puede ocupar media pantalla antes del
   texto. `<details class="toc toc--collapsible">` lo cierra sin JS.  */

.toc--collapsible > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  color: #525252;
}

.toc--collapsible > summary::-webkit-details-marker {
  display: none;
}

.toc--collapsible > summary::after {
  content: '';
  width: 0.5em;
  height: 0.5em;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  rotate: 45deg;
  transition: rotate 200ms cubic-bezier(0.22, 1, 0.36, 1);
}

.toc--collapsible[open] > summary::after {
  rotate: 225deg;
}

.toc--collapsible > summary:focus-visible {
  outline: 2px solid #0a0a0a;
  outline-offset: 1.5px;
}

@media (prefers-reduced-motion: reduce) {
  .toc__link,
  .toc--collapsible > summary::after {
    transition-duration: 0ms;
  }
}

Codigo fuente

styles/components/toc.css
/* -------------------------------------------------------------------------
   Table of Contents

   Indice de un documento largo: rail de links generados desde los headings del
   contenido. A diferencia de `progress-nav` (secciones declaradas a mano, con
   indicador deslizante), aqui los links NO se escriben: el behavior
   `table-of-contents` de packages/animations los clona desde un template y
   marca el activo al hacer scroll.

   Contrato de estado (lo escribe el behavior, no el consumidor):
     [data-toc-status="active"]  link de la seccion visible
     [data-toc-depth="2|3|4"]    nivel del heading, para la indentacion

   El scroll-spy es el behavior; este archivo solo pinta el estado.
   ------------------------------------------------------------------------- */

.toc {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-4);
}

.toc__label {
  color: var(--muted-foreground);
}

.toc__list {
  display: flex;
  flex-direction: column;
}

.toc__link {
  display: block;
  padding-block: var(--spacing-2);
  padding-inline: var(--spacing-4) var(--spacing-2);
  /* El rail es un borde por link, no un borde en la lista: asi el tramo activo
     se pinta sin que un pseudo-elemento tenga que medir posiciones. */
  border-inline-start: var(--stroke-thin) solid var(--border);
  color: var(--muted-foreground);
  font-family: var(--font-family-sans);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-sm);
  text-decoration: none;
  text-wrap: pretty;
  /* Un heading legal puede traer una palabra larguisima o una URL: en un rail de
     15rem tiene que partir, no sobresalir. */
  overflow-wrap: break-word;
  transition:
    color var(--duration-200) var(--easing-out),
    border-color var(--duration-200) var(--easing-out);
}

@media (hover: hover) and (pointer: fine) {
  .toc__link:hover {
    color: var(--foreground);
    border-inline-start-color: var(--muted-foreground);
  }
}

.toc__link[data-toc-status='active'] {
  color: var(--foreground);
  font-weight: var(--font-weight-medium);
  /* Naranja de marca como acento fino: 1.5px de rail, jamas relleno ni texto. */
  border-inline-start-color: var(--brand);
}

.toc__link:focus-visible {
  outline: var(--ring-width) solid var(--ring);
  outline-offset: var(--ring-offset);
  border-radius: var(--radius-sm);
}

/* ---- Profundidad ----
   Se indenta el padding, no el margen: el rail (borde) debe quedar alineado en
   todos los niveles, si no la linea se rompe en cada sub-seccion. */

.toc__link[data-toc-depth='3'] {
  padding-inline-start: calc(var(--spacing-4) + var(--spacing-4));
}

.toc__link[data-toc-depth='4'] {
  padding-inline-start: calc(var(--spacing-4) + var(--spacing-8));
}

/* ---- Colapsado (movil) ----
   En pantallas angostas el indice no puede ocupar media pantalla antes del
   texto. `<details class="toc toc--collapsible">` lo cierra sin JS.  */

.toc--collapsible > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-2);
  cursor: pointer;
  list-style: none;
  color: var(--muted-foreground);
}

.toc--collapsible > summary::-webkit-details-marker {
  display: none;
}

.toc--collapsible > summary::after {
  content: '';
  width: 0.5em;
  height: 0.5em;
  border-inline-end: var(--stroke-thin) solid currentColor;
  border-block-end: var(--stroke-thin) solid currentColor;
  rotate: 45deg;
  transition: rotate var(--duration-200) var(--easing-out);
}

.toc--collapsible[open] > summary::after {
  rotate: 225deg;
}

.toc--collapsible > summary:focus-visible {
  outline: var(--ring-width) solid var(--ring);
  outline-offset: var(--ring-offset);
}

@media (prefers-reduced-motion: reduce) {
  .toc__link,
  .toc--collapsible > summary::after {
    transition-duration: 0ms;
  }
}

On this page

Detalles

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

Componente

Toc

Source

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