A short label in ink, shown on hover, focus and long press.
On this page
A tooltip is a short label in ink that names a control or shows its shortcut. It is a hint popover: put popover="hint" and .tooltip on the label, and interestfor on the control. The browser shows it after a short delay on hover and on keyboard focus, and on a long press on a touch screen; it hides it on Esc, and never closes a menu that is already open. CSS anchor positioning centers it above the control.
A tooltip sits 6px above its control and flips below when there is no room. tooltip-bottom prefers the space below, for controls in a top bar.
Below
<buttontype="button"class="btn"interestfor="tip-deploy">Deploy</button><divid="tip-deploy"popover="hint"class="tooltip tooltip-bottom">Ships the main branch to production</div>
Keep a tooltip to a few words. It wraps at 16rem and balances its lines. A kbd inside reads on the ink surface without a variant, because the tooltip retunes the tokens the keycap uses. For anything a reader has to act on, such as a link or a form, use a popover.
A tooltip adds to a control's name; it does not replace it. Keep aria-label on an icon-only button, since touch screens show a tooltip only on a long press.
The browser connects the tooltip to its control, so assistive technology announces it as a description.
Interest invokers ship in Chrome and Edge. In a browser without them the tooltip does not appear and the control keeps its accessible name. To show it there as well, call showPopover() and hidePopover() from pointer and focus handlers; the stylesheet is the same.
The tooltip component joins a control to its tooltip by id. The control stays the page's own markup, and the browser shows the tooltip. Copy it into the project with npx master-ui add tooltip; React, Svelte and Vue says how the components are built and used.
ui/tooltip.tsx
import{ useId,type ComponentProps,type ReactNode }from'react'exportinterfaceTooltipPropsextendsOmit<ComponentProps<'div'>,'popover'>{/** The control the tooltip names: spread what it is given on a button or a link of your own. */trigger:(attributes:{interestfor:string})=>ReactNode}/** * A tooltip. The browser shows it on hover and on focus and places it; this component only joins the * control to its label. The children are the label. The control keeps its own accessible name. */exportfunctionTooltip({ trigger, className, id, children,...root }:TooltipProps){const generated =useId()const name = id ?? generatedreturn(<>{trigger({interestfor: name })}<divid={name}popover="hint"className={className ?`tooltip ${className}`:'tooltip'}{...root}>{children}</div></>)}
ui/Tooltip.svelte
<scriptlang="ts"module>importtype{ Snippet }from'svelte'importtype{ HTMLAttributes }from'svelte/elements'exportinterfaceTooltipPropsextendsOmit<HTMLAttributes<HTMLDivElement>,'popover'>{/** The control the tooltip names: spread what it is given on a button or a link of your own. */trigger:Snippet<[{interestfor:string}]>}</script><scriptlang="ts">let{ trigger,class: className, id, children,...root }:TooltipProps=$props()const generated =$props.id()const name =$derived(id ?? generated)</script><!--@component A tooltip. The browser shows it on hover and on focus and places it; this component only joins the control to its label. The children are the label. The control keeps its own accessible name.-->{@rendertrigger({interestfor: name })}<divid={name}popover="hint"class={['tooltip', className]}{...root}>{@renderchildren?.()}</div>
ui/Tooltip.vue
<scriptsetuplang="ts">import{ computed, useId }from'vue'defineOptions({inheritAttrs:false})const{ id }=defineProps<{id?:string}>()defineSlots<{/** The control the tooltip names: bind what it is given on a button or a link of your own. */trigger:(attributes:{interestfor:string})=>unknowndefault?:()=>unknown}>()/* * A tooltip. The browser shows it on hover and on focus and places it; this component only joins the * control to its label. The default slot is the label. The control keeps its own accessible name. */const generated =useId()const name =computed(()=> id ?? generated)</script><template><slotname="trigger"v-bind="{interestfor: name }" /><div:id="name"popover="hint"class="tooltip"v-bind="$attrs"><slot /></div></template>
The label, on an element with popover="hint"; without the popover attribute it is never shown. It sits centered above its control at a distance of --anchored-gap (6px), flips below when there is no room, and wraps at 16rem. A kbd inside reads on the ink surface without a variant
tooltip-bottom
Prefers the space below the control, for controls in a top bar. With tooltip
/* * Tooltip: a short label in ink, shown when a control is hovered, focused or long-pressed. * It is a hint popover: popover="hint" on the tooltip and interestfor on the control, so the browser handles * the delay, the keys and touch, and it never closes a menu that is already open. Anchor positioning centers * it above the control and flips it below when there is no room. */@layer components {.tooltip{@apply--on-ink;--anchored-block:block-start;--anchored-inline: span-all;overflow-wrap: anywhere;/* Tokens are retuned for the ink surface, so a kbd inside reads without a variant of its own. */--color-text-muted:color-mix(in oklab,currentColor64%,transparent);--color-line-strong:color-mix(in oklab,currentColor30%,transparent);width:max-content;max-width:min(16rem,calc(100vw-1rem));padding:.3125rem.5rem;border:0;border-radius:calc((var(--radius-chip)+1px)*var(--corner-scale));corner-shape:var(--corner-shape);overflow:visible;background:var(--color-surface-tip);color:light-dark(var(--color-text-inverse),var(--color-text-strong));box-shadow:var(--shadow-tip);font:400.75rem/1.35var(--font-family-sans);font-feature-settings:inherit;text-align:center;text-wrap:balance;transform-origin:bottom;@apply--anchored;@apply--pop;/* Prefer the space below the control. */&.tooltip-bottom{--anchored-block:block-end;transform-origin:top;}@media(forced-colors:active){outline:1pxsolid CanvasText;outline-offset:-1px;}}}