Tooltip
A description that appears on hover or focus, next to what it describes. The pointer waits out a delay — resting on something is ambiguous, and the delay is what disambiguates it — while the keyboard opens at once, because arriving by Tab is a decision already made. A tooltip has no element of its own, so each package attaches it the way that adds nothing to the tree — React clones its child, Vue the vnode, Svelte attaches to the node, Angular is a directive that takes its own attribute back off once it has matched. A wrapper element would port everywhere and be worse: on a flex child it would change the layout it was only meant to annotate. ⚠️ A tooltip on a disabled control never opens, and no wrapper of ours will change that: a disabled element emits no pointer events at all, so nothing fires to open it — which is exactly backwards, because the tooltip on a disabled button is the one that says *why* it is disabled. The answer is aria-disabled="true" instead of disabled, plus an early return in your own handler. The control stays focusable and hoverable, announces itself as disabled, and can be explained; the stylesheet paints it inert and — since this was checked — no longer lets it lift, recolour or press under the pointer.
Code
<button type="button" class="aura-btn aura-btn--secondary" aria-describedby="t1">Delete</button>
<div id="t1" role="tooltip" data-state="open" class="aura-popup aura-tooltip">Delete this space</div>import { Button, Group, IconButton, Tooltip } from '@ilomee/aura-react';
/**
* Hover or focus either control. The panel is portalled to `document.body` and positioned by
* `place()`, so it flips when it does not fit and follows a scroll.
*
* The second one shows `names`: an icon-only button has no text, so there the tooltip is not an
* addition to the name — it is the name, and being both would mean being announced twice.
*/
export default function Demo() {
return (
<Group>
<Tooltip label="Delete this space">
<Button variant="secondary">Delete</Button>
</Tooltip>
<Tooltip label="Settings" names>
{/*
Named here as well as by the tooltip. `names` points `aria-labelledby` at the tooltip
only while it is open, so an icon-only button with nothing else would be nameless for
as long as nobody is hovering it — which is most of the time. `aria-labelledby` wins
while the tooltip is up, so the two never disagree.
*/}
<IconButton aria-label="Settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
</svg>
</IconButton>
</Tooltip>
</Group>
);
}<script setup lang="ts">
import { Tooltip, Button } from '@ilomee/aura-vue';
</script>
<template>
<Tooltip label="Delete this space">
<Button variant="secondary">Delete</Button>
</Tooltip>
</template><script lang="ts">
import { tooltip, Button } from '@ilomee/aura-svelte';
</script>
<Button variant="secondary" {@attach tooltip({ label: 'Delete this space' })}>Delete</Button>An attachment rather than a component, so it adds no element: {@attach tooltip({ label })} on the trigger itself.
Used as [auraTooltip] — a directive you put on your own element.
<button auraButton variant="secondary" auraTooltip="Delete this space">Delete</button>The directive removes its own auraTooltip attribute in ngOnInit, so the rendered markup matches the other three exactly. Angular matches directives at compile time, so taking it back off costs nothing.
Props
| Name | Type | Default | Notes |
|---|---|---|---|
| label* | string | — | Flat text, not a node: a tooltip is an accessible name or description, and both flatten to text — markup inside one is ignored by assistive tech. |
| children* | ReactElement | — | Exactly one element, which must forward ref and spread the props it is handed. Every Aura primitive does. |
| side | 'top' | 'right' | 'bottom' | 'left' | 'top' | |
| align | 'start' | 'center' | 'end' | 'center' | |
| delay | number | 400 | Pointer only. Focus never waits. |
| names | boolean | false | Make the tooltip the trigger’s accessible name (aria-labelledby) instead of an addition to it (aria-describedby). For an icon-only button the tooltip is the only name there is. |
| Name | Type | Default | Notes |
|---|---|---|---|
| label* | string | — | |
| defaultslot | slot | — | Exactly one element. Cloned, not wrapped. |
| side | 'top' | 'right' | 'bottom' | 'left' | 'top' | |
| align | 'start' | 'center' | 'end' | 'center' | |
| delay | number | 400 | |
| names | boolean | false |
| Name | Type | Default | Notes |
|---|---|---|---|
| label* | string | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'top' | |
| align | 'start' | 'center' | 'end' | 'center' | |
| delay | number | 400 | |
| names | boolean | false |
| Name | Type | Default | Notes |
|---|---|---|---|
| auraTooltip* | string | — | The label, named after the selector so <button auraTooltip="Delete"> reads. |
| side | 'top' | 'right' | 'bottom' | 'left' | 'top' | |
| align | 'start' | 'center' | 'end' | 'center' | |
| delay | number | 400 | |
| names | boolean | false |
Emitted classes
Every framework emits the same classes — that is what makes the four libraries look identical. You can use them directly, without any component library.
| Class | Role | Notes |
|---|---|---|
| .aura-popup | base | Required. A modifier does nothing without it. |
| .aura-tooltip | base | Required. A modifier does nothing without it. |
Tokens and rules
components.css- layer
- var(--z-tooltip) — above every other float, on purpose
- width
- min(32ch, calc(100vw - 16px)) — a label, not a surface
- gap to anchor
- var(--popover-offset)