Navigation

v0.0.4
Overlays

Tooltip

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.

Tooltip

<button type="button" class="btn btn-square" interestfor="tip-search" aria-label="Search">  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">    <circle cx="11" cy="11" r="6" />    <path d="m20 20-4.35-4.35" />  </svg></button><button type="button" class="btn btn-square" interestfor="tip-filter" aria-label="Filter">  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">    <path d="M4 6h16M7 12h10M10 18h4" />  </svg></button><div id="tip-search" popover="hint" class="tooltip">Search  <kbd class="kbd">/</kbd></div><div id="tip-filter" popover="hint" class="tooltip">Filter  <kbd class="kbd">F</kbd></div>
Hover a button, or move to it with Tab.

#Placement

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

<button type="button" class="btn" interestfor="tip-deploy">Deploy</button><div id="tip-deploy" popover="hint" class="tooltip tooltip-bottom">Ships the main branch to production</div>

#Content

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.

#Accessibility

  • 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.

#React, Svelte and Vue

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'export interface TooltipProps extends Omit<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. */export function Tooltip({ trigger, className, id, children, ...root }: TooltipProps) {  const generated = useId()  const name = id ?? generated  return (    <>      {trigger({ interestfor: name })}      <div id={name} popover="hint" className={className ? `tooltip ${className}` : 'tooltip'} {...root}>{children}</div>    </>  )}
ui/Tooltip.svelte
<script lang="ts" module>  import type { Snippet } from 'svelte'  import type { HTMLAttributes } from 'svelte/elements'  export interface TooltipProps extends Omit<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><script lang="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.-->{@render trigger({ interestfor: name })}<div id={name} popover="hint" class={['tooltip', className]} {...root}>{@render children?.()}</div>
ui/Tooltip.vue
<script setup lang="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 }) => unknown  default?: () => 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>  <slot name="trigger" v-bind="{ interestfor: name }" />  <div :id="name" popover="hint" class="tooltip" v-bind="$attrs"><slot /></div></template>

#Classes

ClassDescription
tooltipThe 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-bottomPrefers the space below the control, for controls in a top bar. With tooltip

#Custom properties

PropertyDefaultDescription
--anchored-gapSpace between the surface and the control that opened it; 6px when not set. On tooltip

#Source

@master/ui/components/tooltip.css
/* * 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, currentColor 64%, transparent);    --color-line-strong: color-mix(in oklab, currentColor 30%, 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.35 var(--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: 1px solid CanvasText;      outline-offset: -1px;    }  }}