← Todos los specs UI

Contrato del componente

tooltip

Patrón WAI-ARIA Tooltip (scratch build) — envuelve el primer Element child del consumidor como trigger, abre en hover (con openDelay) y en focus (inmediato), se cierra con Escape sin sacar el foco del trigger, y también en blur o mouseleave real. shadow:false para que aria-describedby del trigger pueda apuntar al id del tooltip.

Versión
v1.0.0

Props

NombreTipoValoresPredeterminado
labelstring
openDelaynumber250
closeDelaynumber100
placementenumtop, bottombottom

Slot

Nombre
default
Propósito
the trigger — the first Element child inside ds-tooltip is treated as the trigger and MUST be focusable (native button/link/form control, or an element with tabindex=0). The tooltip is rendered as a sibling <span role="tooltip"> at the end of the host's children.

Tokens

spacing
xs, sm
radius
sm
font
family, size-md

Máquina de estados

Estado inicial: closed

closed

MOUSE_ENTER
→ opening · start_open_timer(openDelay)
FOCUS_IN
→ open · show_immediately

opening

TIMER_ELAPSED
→ open · show
MOUSE_LEAVE
→ closed · cancel_open_timer

open

MOUSE_LEAVE
→ closing · start_close_timer(closeDelay)
FOCUS_OUT
→ closed · hide_immediately
ESCAPE
→ closed · hide_immediately, keep_focus_on_trigger

closing

TIMER_ELAPSED
→ closed · hide
MOUSE_ENTER
→ open · cancel_close_timer

Accesibilidad

native_element
span[role=tooltip] alongside the consumer trigger (scratch-built)
apg_pattern
https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/
keyboard
["Focus (Tab into trigger) → tooltip opens immediately (no openDelay for keyboard)","Escape (while open) → dismisses without moving focus; trigger remains focused (WCAG 1.4.13 dismissable)","Blur (Tab out of trigger) → tooltip closes immediately","Enter / Space / arrow / printable → forwarded to trigger untouched"]
pointer
["mouseenter host → open after openDelay (ergonomic against twitchy tooltips)","mouseleave host → close after closeDelay","re-entering during closeDelay cancels the hide (hoverable — WCAG 1.4.13)"]
aria
{"tooltip_element":"role=tooltip, unique id per instance (auto-generated to avoid collisions on pages with multiple tooltips)","trigger":"aria-describedby=<tooltipId> set imperatively on the first Element child in componentDidLoad"}
wcag_1_4_13_content_on_hover_or_focus
{"dismissable":"Escape hides the tooltip without moving focus","hoverable":"closeDelay + cancel-on-re-enter allow the pointer to move onto the tooltip surface without dismissing it","persistent":"tooltip stays visible while trigger is focused or hovered and while the pointer is over the tooltip surface"}
consumer_contract
["The first Element child inside <ds-tooltip> MUST be focusable — otherwise keyboard users cannot summon the tooltip and the pattern fails WCAG 1.4.13. The component does not force focusability onto arbitrary children.","`label` is REQUIRED — a tooltip with no accessible text is inert.","The tooltip is describedby, NOT labelledby — it supplements the trigger's own accessible name (which comes from the trigger's text or aria-label)."]
notes
Scratch-built per the a11y spec §D ceremony because there is no native tooltip element. shadow:false is intentional: aria-describedby needs to reference the tooltip's id from the light-DOM trigger, and shadow-scoped ids are unreachable.
pending_manual_verification
["VoiceOver on macOS: confirm the tooltip text is announced as a description of the trigger when the trigger gains focus","VoiceOver: confirm Escape dismisses the tooltip and leaves the trigger focused; re-focusing brings it back","Chromium reduced-motion: confirm no residual animation is added by the component"]
JSON validado original
{
  "name": "tooltip",
  "version": "1.0.0",
  "props": {
    "label": {
      "type": "string"
    },
    "openDelay": {
      "type": "number",
      "default": 250
    },
    "closeDelay": {
      "type": "number",
      "default": 100
    },
    "placement": {
      "type": "enum",
      "values": [
        "top",
        "bottom"
      ],
      "default": "bottom"
    }
  },
  "slot": {
    "name": "default",
    "purpose": "the trigger — the first Element child inside ds-tooltip is treated as the trigger and MUST be focusable (native button/link/form control, or an element with tabindex=0). The tooltip is rendered as a sibling <span role=\"tooltip\"> at the end of the host's children."
  },
  "state_machine": {
    "initial": "closed",
    "states": {
      "closed": {
        "on": {
          "MOUSE_ENTER": {
            "target": "opening",
            "actions": [
              "start_open_timer(openDelay)"
            ]
          },
          "FOCUS_IN": {
            "target": "open",
            "actions": [
              "show_immediately"
            ]
          }
        }
      },
      "opening": {
        "on": {
          "TIMER_ELAPSED": {
            "target": "open",
            "actions": [
              "show"
            ]
          },
          "MOUSE_LEAVE": {
            "target": "closed",
            "actions": [
              "cancel_open_timer"
            ]
          }
        }
      },
      "open": {
        "on": {
          "MOUSE_LEAVE": {
            "target": "closing",
            "actions": [
              "start_close_timer(closeDelay)"
            ]
          },
          "FOCUS_OUT": {
            "target": "closed",
            "actions": [
              "hide_immediately"
            ]
          },
          "ESCAPE": {
            "target": "closed",
            "actions": [
              "hide_immediately",
              "keep_focus_on_trigger"
            ]
          }
        }
      },
      "closing": {
        "on": {
          "TIMER_ELAPSED": {
            "target": "closed",
            "actions": [
              "hide"
            ]
          },
          "MOUSE_ENTER": {
            "target": "open",
            "actions": [
              "cancel_close_timer"
            ]
          }
        }
      }
    }
  },
  "tokens_used": {
    "spacing": [
      "xs",
      "sm"
    ],
    "radius": [
      "sm"
    ],
    "font": [
      "family",
      "size-md"
    ]
  },
  "a11y": {
    "native_element": "span[role=tooltip] alongside the consumer trigger (scratch-built)",
    "apg_pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/",
    "keyboard": [
      "Focus (Tab into trigger) → tooltip opens immediately (no openDelay for keyboard)",
      "Escape (while open) → dismisses without moving focus; trigger remains focused (WCAG 1.4.13 dismissable)",
      "Blur (Tab out of trigger) → tooltip closes immediately",
      "Enter / Space / arrow / printable → forwarded to trigger untouched"
    ],
    "pointer": [
      "mouseenter host → open after openDelay (ergonomic against twitchy tooltips)",
      "mouseleave host → close after closeDelay",
      "re-entering during closeDelay cancels the hide (hoverable — WCAG 1.4.13)"
    ],
    "aria": {
      "tooltip_element": "role=tooltip, unique id per instance (auto-generated to avoid collisions on pages with multiple tooltips)",
      "trigger": "aria-describedby=<tooltipId> set imperatively on the first Element child in componentDidLoad"
    },
    "wcag_1_4_13_content_on_hover_or_focus": {
      "dismissable": "Escape hides the tooltip without moving focus",
      "hoverable": "closeDelay + cancel-on-re-enter allow the pointer to move onto the tooltip surface without dismissing it",
      "persistent": "tooltip stays visible while trigger is focused or hovered and while the pointer is over the tooltip surface"
    },
    "consumer_contract": [
      "The first Element child inside <ds-tooltip> MUST be focusable — otherwise keyboard users cannot summon the tooltip and the pattern fails WCAG 1.4.13. The component does not force focusability onto arbitrary children.",
      "`label` is REQUIRED — a tooltip with no accessible text is inert.",
      "The tooltip is describedby, NOT labelledby — it supplements the trigger's own accessible name (which comes from the trigger's text or aria-label)."
    ],
    "notes": "Scratch-built per the a11y spec §D ceremony because there is no native tooltip element. shadow:false is intentional: aria-describedby needs to reference the tooltip's id from the light-DOM trigger, and shadow-scoped ids are unreachable.",
    "pending_manual_verification": [
      "VoiceOver on macOS: confirm the tooltip text is announced as a description of the trigger when the trigger gains focus",
      "VoiceOver: confirm Escape dismisses the tooltip and leaves the trigger focused; re-focusing brings it back",
      "Chromium reduced-motion: confirm no residual animation is added by the component"
    ]
  }
}