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.
Anatomía CSS
<div class="toc"> <span class="toc__label"></span> <span class="toc__link"></span> <span class="toc__list"></span> </div>
| Clase | Propósito |
|---|---|
toc | root |
toc--collapsible | modifier |
toc__label | element |
toc__link | element |
toc__list | 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 | #0a0a0a | foreground |
| all | hover-fg | #0a0a0a | foreground |
| collapsible | fg | #525252 | muted.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
color | var(--duration-200) | var(--easing-out) |
border-color | var(--duration-200) | var(--easing-out) |
rotate | 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).
/* -------------------------------------------------------------------------
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; } }