Tooltip
Hover or focus and a short hint appears beside the control. Labels for icon-only actions.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
```html <!-- CSS-only: casos simples, un trigger, cero JS --> <button data-tooltip="Guardar cambios" data-tooltip-dir="top">Guardar</button> ```
```html <!-- Smart (behavior tooltip.ts): grupos que viajan con Flip, edge detection --> <div data-tooltip-smart data-tooltip-placement="top" data-tooltip-delay="0" data-tooltip-hide-delay="100"> <button data-tooltip-trigger data-tooltip-group="filters" data-tooltip-content="Filtra por color">Color</button> <button data-tooltip-trigger data-tooltip-group="filters" data-tooltip-content="Solo ofertas">Sale</button> </div> <!-- + initTooltipSmart() de @atom-uikit/animations (AtomMotion.initAll() lo cubre) --> ```
## Cuál de los dos
| Caso | Mecanismo | |---|---| | Un hint sobre un icono o botón suelto | **CSS-only** (`data-tooltip`) — cero JS, cero costo | | Fila de filtros/acciones donde el tooltip debe VIAJAR entre triggers | **Smart** (`data-tooltip-smart`) — Flip por grupo | | Trigger pegado al borde del viewport | **Smart** — detecta bordes y se voltea solo | | Necesitas delay de aparición o de salida | **Smart** (`data-tooltip-delay` / `data-tooltip-hide-delay`, en ms) |
## Accesibilidad
- El smart es **funcional** bajo `prefers-reduced-motion`: aparece y desaparece instantáneo (el original del que se adaptó lo apagaba por completo). - `aria-describedby` real del trigger al popup, y **Escape** cierra sin mover el puntero (WCAG 1.4.13). Focus/blur equivalen a hover para teclado. - El popup del smart usa `--primary`/`--primary-foreground`: inverso clásico y theme-aware — en dark el CSS-only (neutral-900 fijo) contrasta peor; para superficies dark prefiere el smart o la variante `data-tooltip-theme="light"`.
## Cuándo no usar
- Contenido interactivo (links, botones dentro del popup) → usa `Popover`/`DropdownMenu`; un tooltip es solo lectura y `pointer-events: none`. - Información imprescindible para completar la tarea → ponla visible; el hover no existe en táctil. - Textos largos o multilínea en desktop → usa un `Dialog` o texto inline.
Anatomía CSS
<div class="is-bottom"> </div>
| Clase | Propósito |
|---|---|
is-bottom | root |
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 | #0a0a0a | primary |
| all | fg | #fafafa | primary.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
opacity | 0.25s | cubic-bezier(0.625 |
0.05 | | |
0 | | |
1) | | |
transform | 0.25s | cubic-bezier(0.625 |
0.05 | | |
0 | | |
1) | | |
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).
/* -------------------------------------------------------------------------
Tooltip (CSS Only)
Small text hint on hover. Attach to any element.
Direction: top (default), bottom, left, right
Theme: dark (default), light
Usage: wrap trigger in [data-tooltip] with content as attribute value.
<button data-tooltip="Save changes" data-tooltip-dir="top">Save</button>
------------------------------------------------------------------------- */
[data-tooltip] {
position: relative;
}
[data-tooltip]::before,
[data-tooltip]::after {
position: absolute;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.25s cubic-bezier(0.625, 0.05, 0, 1),
transform 0.25s cubic-bezier(0.625, 0.05, 0, 1);
z-index: 70;
}
/* Text bubble */
[data-tooltip]::before {
content: attr(data-tooltip);
padding: 4px 8px;
border-radius: 6px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
max-width: 200px;
}
/* Arrow */
[data-tooltip]::after {
content: '';
width: 6px;
height: 6px;
transform: rotate(45deg);
}
/* ---- Dark theme (default) ---- */
[data-tooltip]::before {
background-color: #171717;
color: #fafafa;
}
[data-tooltip]::after {
background-color: #171717;
}
/* ---- Light theme ---- */
[data-tooltip-theme="light"]::before {
background-color: #fafafa;
color: #0a0a0a;
border: 1px solid #e5e5e5;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
[data-tooltip-theme="light"]::after {
background-color: #fafafa;
border: 1px solid #e5e5e5;
border-top: none;
border-left: none;
}
/* ---- Show on hover ---- */
[data-tooltip]:hover::before,
[data-tooltip]:hover::after {
opacity: 1;
visibility: visible;
}
/* ---- Top (default) ---- */
[data-tooltip]::before,
[data-tooltip-dir="top"]::before {
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
}
[data-tooltip]:hover::before,
[data-tooltip-dir="top"]:hover::before {
transform: translateX(-50%) translateY(0);
}
[data-tooltip]::after,
[data-tooltip-dir="top"]::after {
bottom: calc(100% + 5px);
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
/* ---- Bottom ---- */
[data-tooltip-dir="bottom"]::before {
bottom: auto;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(-4px);
}
[data-tooltip-dir="bottom"]:hover::before {
transform: translateX(-50%) translateY(0);
}
[data-tooltip-dir="bottom"]::after {
bottom: auto;
top: calc(100% + 5px);
left: 50%;
transform: translateX(-50%) rotate(225deg);
}
/* ---- Left ---- */
[data-tooltip-dir="left"]::before {
bottom: auto;
top: 50%;
right: calc(100% + 8px);
left: auto;
transform: translateY(-50%) translateX(4px);
}
[data-tooltip-dir="left"]:hover::before {
transform: translateY(-50%) translateX(0);
}
[data-tooltip-dir="left"]::after {
bottom: auto;
top: 50%;
right: calc(100% + 5px);
left: auto;
transform: translateY(-50%) rotate(315deg);
}
/* ---- Right ---- */
[data-tooltip-dir="right"]::before {
bottom: auto;
top: 50%;
left: calc(100% + 8px);
transform: translateY(-50%) translateX(-4px);
}
[data-tooltip-dir="right"]:hover::before {
transform: translateY(-50%) translateX(0);
}
[data-tooltip-dir="right"]::after {
bottom: auto;
top: 50%;
left: calc(100% + 5px);
transform: translateY(-50%) rotate(135deg);
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
[data-tooltip]::before,
[data-tooltip]::after {
transition-duration: 0ms;
}
}
/* -------------------------------------------------------------------------
Smart tooltip (behavior tooltip.ts) — popup real en portal
El popup lo crea el behavior en runtime (por eso el JS le pone la clase en
ambas variantes, con y sin ds-). Edge detection, delays y el viaje con
Flip entre triggers del mismo grupo viven en
packages/animations/src/tooltip.ts; aqui SOLO pintura.
Colores: --primary/--primary-foreground = el inverso clasico de tooltip,
theme-aware gratis (oscuro sobre light, claro sobre dark) — mejora el
contraste en dark frente al CSS-only de arriba (neutral-900 fijo).
------------------------------------------------------------------------- */
.tooltip__popup {
position: fixed;
z-index: 70;
pointer-events: none;
will-change: transform, opacity;
}
.tooltip__popup-bg {
position: absolute;
inset: 0;
background-color: #0a0a0a;
border-radius: 8px;
will-change: transform;
}
.tooltip__popup-text {
position: relative;
z-index: 1;
padding: 4px 8px;
color: #fafafa;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
}
/* transform-origin por placement: la entrada escala DESDE el trigger */
.tooltip__popup.is-top {
transform-origin: bottom center;
}
.tooltip__popup.is-bottom {
transform-origin: top center;
}
.tooltip__popup.is-left {
transform-origin: right center;
}
.tooltip__popup.is-right {
transform-origin: left center;
}
@media (max-width: 479px) {
.tooltip__popup-text {
max-width: calc(100vw - 32px);
white-space: normal;
}
}
Codigo fuente
/* ------------------------------------------------------------------------- Tooltip (CSS Only) Small text hint on hover. Attach to any element. Direction: top (default), bottom, left, right Theme: dark (default), light Usage: wrap trigger in [data-tooltip] with content as attribute value. <button data-tooltip="Save changes" data-tooltip-dir="top">Save</button> ------------------------------------------------------------------------- */ [data-tooltip] { position: relative; } [data-tooltip]::before, [data-tooltip]::after { position: absolute; opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.25s cubic-bezier(0.625, 0.05, 0, 1), transform 0.25s cubic-bezier(0.625, 0.05, 0, 1); z-index: var(--z-index-70, 70); } /* Text bubble */ [data-tooltip]::before { content: attr(data-tooltip); padding: 4px 8px; border-radius: 6px; font-family: var(--font-family-sans), ui-sans-serif, system-ui, sans-serif; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); line-height: 1.4; white-space: nowrap; max-width: 200px; } /* Arrow */ [data-tooltip]::after { content: ''; width: 6px; height: 6px; transform: rotate(45deg); } /* ---- Dark theme (default) ---- */ [data-tooltip]::before { background-color: var(--color-neutral-900); color: var(--color-neutral-50); } [data-tooltip]::after { background-color: var(--color-neutral-900); } /* ---- Light theme ---- */ [data-tooltip-theme="light"]::before { background-color: var(--background); color: var(--foreground); border: 1px solid var(--border); box-shadow: var(--shadow-sm); } [data-tooltip-theme="light"]::after { background-color: var(--background); border: 1px solid var(--border); border-top: none; border-left: none; } /* ---- Show on hover ---- */ [data-tooltip]:hover::before, [data-tooltip]:hover::after { opacity: 1; visibility: visible; } /* ---- Top (default) ---- */ [data-tooltip]::before, [data-tooltip-dir="top"]::before { bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px); } [data-tooltip]:hover::before, [data-tooltip-dir="top"]:hover::before { transform: translateX(-50%) translateY(0); } [data-tooltip]::after, [data-tooltip-dir="top"]::after { bottom: calc(100% + 5px); left: 50%; transform: translateX(-50%) rotate(45deg); } /* ---- Bottom ---- */ [data-tooltip-dir="bottom"]::before { bottom: auto; top: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(-4px); } [data-tooltip-dir="bottom"]:hover::before { transform: translateX(-50%) translateY(0); } [data-tooltip-dir="bottom"]::after { bottom: auto; top: calc(100% + 5px); left: 50%; transform: translateX(-50%) rotate(225deg); } /* ---- Left ---- */ [data-tooltip-dir="left"]::before { bottom: auto; top: 50%; right: calc(100% + 8px); left: auto; transform: translateY(-50%) translateX(4px); } [data-tooltip-dir="left"]:hover::before { transform: translateY(-50%) translateX(0); } [data-tooltip-dir="left"]::after { bottom: auto; top: 50%; right: calc(100% + 5px); left: auto; transform: translateY(-50%) rotate(315deg); } /* ---- Right ---- */ [data-tooltip-dir="right"]::before { bottom: auto; top: 50%; left: calc(100% + 8px); transform: translateY(-50%) translateX(-4px); } [data-tooltip-dir="right"]:hover::before { transform: translateY(-50%) translateX(0); } [data-tooltip-dir="right"]::after { bottom: auto; top: 50%; left: calc(100% + 5px); transform: translateY(-50%) rotate(135deg); } /* ---- Reduced motion ---- */ @media (prefers-reduced-motion: reduce) { [data-tooltip]::before, [data-tooltip]::after { transition-duration: 0ms; } } /* ------------------------------------------------------------------------- Smart tooltip (behavior tooltip.ts) — popup real en portal El popup lo crea el behavior en runtime (por eso el JS le pone la clase en ambas variantes, con y sin ds-). Edge detection, delays y el viaje con Flip entre triggers del mismo grupo viven en packages/animations/src/tooltip.ts; aqui SOLO pintura. Colores: --primary/--primary-foreground = el inverso clasico de tooltip, theme-aware gratis (oscuro sobre light, claro sobre dark) — mejora el contraste en dark frente al CSS-only de arriba (neutral-900 fijo). ------------------------------------------------------------------------- */ .tooltip__popup { position: fixed; z-index: var(--z-index-70, 70); pointer-events: none; will-change: transform, opacity; } .tooltip__popup-bg { position: absolute; inset: 0; background-color: var(--primary); border-radius: var(--radius-md); will-change: transform; } .tooltip__popup-text { position: relative; z-index: 1; padding: var(--spacing-1) var(--spacing-2); color: var(--primary-foreground); font-family: var(--font-family-sans), ui-sans-serif, system-ui, sans-serif; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); line-height: 1.4; white-space: nowrap; } /* transform-origin por placement: la entrada escala DESDE el trigger */ .tooltip__popup.is-top { transform-origin: bottom center; } .tooltip__popup.is-bottom { transform-origin: top center; } .tooltip__popup.is-left { transform-origin: right center; } .tooltip__popup.is-right { transform-origin: left center; } @media (max-width: 479px) { .tooltip__popup-text { max-width: calc(100vw - var(--spacing-8)); white-space: normal; } }