Skip to content

Communication

Tooltips

Plain tooltips name a control; rich tooltips explain it, optionally with an action.

Examples

Plain and rich

Hover, focus or long-press the buttons. A rich tooltip with an action is a non-modal dialog that Tab moves into.

The live demo runs with JavaScript. Its code is below.

React HTML
import {
  Icon,
  IconButton,
  PlainTooltip,
  RichTooltip,
  TooltipTrigger,
} from '@kyralabs/m3exp-web/react';

export default function Tooltips() {
  return (
    <>
      <TooltipTrigger>
        <IconButton aria-label="Favorite">
          <Icon name="favorite" />
        </IconButton>
        <PlainTooltip>Add to favorites</PlainTooltip>
      </TooltipTrigger>
      <TooltipTrigger isPersistent>
        <IconButton aria-label="Permissions">
          <Icon name="info" />
        </IconButton>
        <RichTooltip subhead="Permissions" action="Request access" caret>
          Configure permissions for selected service accounts.
        </RichTooltip>
      </TooltipTrigger>
    </>
  );
}
<button id="fav" type="button" class="m3e-icon-button" aria-label="Favorite">
  <span class="m3e-icon" aria-hidden="true">favorite</span>
</button>
<m3e-tooltip anchor="fav">Add to favorites</m3e-tooltip>

<button id="perms" type="button" class="m3e-icon-button" aria-label="Permissions">
  <span class="m3e-icon" aria-hidden="true">info</span>
</button>
<m3e-tooltip
  anchor="perms"
  variant="rich"
  subhead="Permissions"
  action="Request access"
  caret
  persistent
>
  Configure permissions for selected service accounts.
</m3e-tooltip>

Run the HTML version (custom elements, no framework).

API

React

<TooltipTrigger>

Attaches a tooltip to an anchor (COMPONENT_SPEC §33; Compose TooltipBox). Mouse hover, keyboard focus and a touch long press show it, Escape and a click outside hide it, and only one tooltip is shown at a time — the m3e-elements TooltipController, shared with <m3e-tooltip>. The anchor gets aria-describedby, unless the tooltip text equals its accessible name (then the tooltip is aria-hidden).

Prop Type Default Description
children required [ReactElement, ReactElement] | ReactNode Two children: the anchor (a component that forwards its ref to a DOM element, such as <IconButton> or <Button>) and a <PlainTooltip> or <RichTooltip>.
position "start" | "end" | "below" | "above" | "left" | "right" above TooltipAnchorPosition. Default above (flips below at the window top).
isPersistent boolean TooltipState.isPersistent: stays until a click outside or Escape (leaving the anchor does not hide it, nor does the duration). Default false, as rememberTooltipState.
isDisabled boolean No hover / focus / long-press handling (TooltipBox enableUserInput = false).
isOpen boolean
defaultOpen boolean
onOpenChange (isOpen: boolean) => void

<PlainTooltip>

Plain tooltip (COMPONENT_SPEC §33; Compose PlainTooltip): a short description of its anchor, in a <TooltipTrigger>. Inverse surface, extra-small corners, bodySmall, up to 200dp wide.

Prop Type Default Description
children required ReactNode The tooltip text.
caret boolean TooltipDefaults.caretShape(): a caret on the edge facing the anchor.

Also accepts HTML attributes not listed here.

<RichTooltip>

Rich tooltip (COMPONENT_SPEC §33; Compose RichTooltip): an optional subhead, the supporting text and an optional action, in a <TooltipTrigger> (usually isPersistent). Surface container, medium corners, a level 2 shadow, up to 320dp wide. With an action it is a non-modal dialog named by the subhead (or the anchor) that Tab from the anchor moves into.

Prop Type Default Description
subhead ReactNode The title (RichTooltip title).
action ReactNode The label of the action (a text button). With it the tooltip is a non-modal dialog.
children required ReactNode The tooltip text.
caret boolean TooltipDefaults.caretShape(): a caret on the edge facing the anchor.
onAction () => void The action was pressed (the tooltip then hides).

Also accepts HTML attributes not listed here.

Custom elements

<m3e-tooltip>

import '@kyralabs/m3e-elements/tooltip.js';

<m3e-tooltip anchor="favorite">Add to favorites</m3e-tooltip> (COMPONENT_SPEC §33). Renders the m3e-css .m3e-tooltip contract inside itself as a popover="manual", moving its own child nodes into it as the tooltip text, and runs Compose's TooltipBox behavior through TooltipController (hover, keyboard focus, long press, Escape, a click outside, one tooltip at a time) with the TooltipPositionProviderImpl placement and show / hide springs.

  • anchor: id of the element the tooltip describes (it gets aria-describedby, unless the tooltip text equals its accessible name: the tooltip is then aria-hidden); anchorElement sets it directly.
  • variant: plain (default) | rich. A rich tooltip takes subhead (its title) and action (the label of a text button; clicking it fires action and, unless that is cancelled, hides the tooltip). A tooltip with an action is a non-modal dialog named by its subhead (or the anchor) that Tab from the anchor moves into.
  • position: above (default) | below | left | right | start | end (TooltipAnchorPosition). caret draws TooltipDefaults.caretShape().
  • persistent: TooltipState.isPersistent — stays until a click outside, Escape or hide() (leaving the anchor does not hide it, nor does the duration).
  • manual: no hover / focus / long press handling (TooltipBox enableUserInput = false); only open, show() and hide() control it.
  • open (reflected): whether it is shown. show() is TooltipState.show(): a non-persistent tooltip hides again after --m3e-comp-tooltip-duration. hide() is TooltipState.dismiss().
  • Events: open-change with detail: { open }; action (cancelable) from the action button.
Attribute Property Type Notes
open open boolean Reflected.
anchor anchor string
variant variant string
position position string
caret caret boolean
persistent persistent boolean
manual manual boolean
subhead subhead string
action action string
Event Detail Description
open-change OpenChangeDetail Shown or hidden (after the change).
action null The rich tooltip's action button was pressed; cancelable (preventDefault() keeps it shown).

CSS

tooltip.css

@import '@kyralabs/m3e-css/tooltip.css';

Tooltips (COMPONENT_SPEC §33): plain and rich, with an optional caret. CSS draws them; the m3e-elements tooltip controller (<m3e-tooltip>, m3e-react's <TooltipTrigger>) shows them on hover, keyboard focus and long press, places them next to the anchor and springs them in and out.

<div class="m3e-tooltip" role="tooltip" id="t1" popover="manual">Add to favorites</div>

<div class="m3e-tooltip m3e-tooltip--rich" role="tooltip" id="t2" popover="manual">
  <div class="m3e-tooltip__subhead">Permissions</div>
  <div class="m3e-tooltip__supporting-text">Configure permissions for …</div>
  <div class="m3e-tooltip__action">
    <button class="m3e-button m3e-button--text m3e-button--size-s m3e-button--round"
      type="button">Request access</button>
  </div>
</div>
  • Plain: the text is the tooltip's content. Rich (--rich): an optional subhead, the supporting
text and an optional action slot (a text button: import button.css as well).
  • --caret: DefaultTooltipCaretShape, a TooltipDefaults.caretSize triangle on the edge facing the
anchor. Without the controller it sits under the middle of the bottom edge (a tooltip above
its anchor); the controller writes [data-caret] (up | down | left | right: where the apex
points) and --_m3e-tooltip-caret-x / -y (the middle of its base, px from the top-left corner).
  • Without [popover] the tooltip renders inline (static specimens).
  • JS hooks: [data-driven] (show / hide springs write --_m3e-tooltip-scale / -opacity),
[data-positioned] (TooltipPositionProviderImpl writes --_m3e-tooltip-x / -y).
53 custom properties it reads
Custom property Default
--m3e-comp-divider-thickness 1px
--m3e-comp-minimum-interactive-component-size 48px
--m3e-comp-plain-tooltip-container-color var(--m3e-sys-color-inverse-surface)
--m3e-comp-plain-tooltip-container-shape var(--m3e-sys-shape-corner-extra-small)
--m3e-comp-plain-tooltip-supporting-text-color var(--m3e-sys-color-inverse-on-surface)
--m3e-comp-plain-tooltip-supporting-text-font var(--m3e-sys-typescale-body-small-font)
--m3e-comp-plain-tooltip-supporting-text-line-height var(--m3e-sys-typescale-body-small-line-height)
--m3e-comp-plain-tooltip-supporting-text-size var(--m3e-sys-typescale-body-small-size)
--m3e-comp-plain-tooltip-supporting-text-tracking var(--m3e-sys-typescale-body-small-tracking)
--m3e-comp-plain-tooltip-supporting-text-weight var(--m3e-sys-typescale-body-small-weight)
--m3e-comp-rich-tooltip-action-label-text-color var(--m3e-sys-color-primary)
--m3e-comp-rich-tooltip-action-label-text-font var(--m3e-sys-typescale-label-large-font)
--m3e-comp-rich-tooltip-action-label-text-line-height var(--m3e-sys-typescale-label-large-line-height)
--m3e-comp-rich-tooltip-action-label-text-size var(--m3e-sys-typescale-label-large-size)
--m3e-comp-rich-tooltip-action-label-text-tracking var(--m3e-sys-typescale-label-large-tracking)
--m3e-comp-rich-tooltip-action-label-text-weight var(--m3e-sys-typescale-label-large-weight)
--m3e-comp-rich-tooltip-container-color var(--m3e-sys-color-surface-container)
--m3e-comp-rich-tooltip-container-shape var(--m3e-sys-shape-corner-medium)
--m3e-comp-rich-tooltip-subhead-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-rich-tooltip-subhead-font var(--m3e-sys-typescale-title-small-font)
--m3e-comp-rich-tooltip-subhead-line-height var(--m3e-sys-typescale-title-small-line-height)
--m3e-comp-rich-tooltip-subhead-size var(--m3e-sys-typescale-title-small-size)
--m3e-comp-rich-tooltip-subhead-tracking var(--m3e-sys-typescale-title-small-tracking)
--m3e-comp-rich-tooltip-subhead-weight var(--m3e-sys-typescale-title-small-weight)
--m3e-comp-rich-tooltip-supporting-text-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-rich-tooltip-supporting-text-font var(--m3e-sys-typescale-body-medium-font)
--m3e-comp-rich-tooltip-supporting-text-line-height var(--m3e-sys-typescale-body-medium-line-height)
--m3e-comp-rich-tooltip-supporting-text-size var(--m3e-sys-typescale-body-medium-size)
--m3e-comp-rich-tooltip-supporting-text-tracking var(--m3e-sys-typescale-body-medium-tracking)
--m3e-comp-rich-tooltip-supporting-text-weight var(--m3e-sys-typescale-body-medium-weight)
--m3e-comp-tooltip-baseline-shift 0.34
--m3e-comp-tooltip-caret-height 8px
--m3e-comp-tooltip-caret-width 16px
--m3e-comp-tooltip-closed-opacity 0
--m3e-comp-tooltip-closed-scale 0.8
--m3e-comp-tooltip-min-height 24px
--m3e-comp-tooltip-min-width 40px
--m3e-comp-tooltip-opacity-duration var(--m3e-sys-motion-spring-fast-effects-duration)
--m3e-comp-tooltip-opacity-easing var(--m3e-sys-motion-spring-fast-effects-easing)
--m3e-comp-tooltip-plain-max-width 200px
--m3e-comp-tooltip-plain-padding-horizontal 8px
--m3e-comp-tooltip-plain-padding-vertical 4px
--m3e-comp-tooltip-rich-action-min-height 36px
--m3e-comp-tooltip-rich-action-padding-bottom 8px
--m3e-comp-tooltip-rich-max-width 320px
--m3e-comp-tooltip-rich-padding-horizontal 16px
--m3e-comp-tooltip-rich-shadow var(--m3e-sys-elevation-level2-shadow)
--m3e-comp-tooltip-rich-subhead-baseline 28px
--m3e-comp-tooltip-rich-text-baseline 24px
--m3e-comp-tooltip-rich-text-padding-bottom 16px
--m3e-comp-tooltip-scale-duration var(--m3e-sys-motion-spring-fast-spatial-duration)
--m3e-comp-tooltip-scale-easing var(--m3e-sys-motion-spring-fast-spatial-easing)
--m3e-comp-tooltip-spacing 4px

Accessibility

role="tooltip", the anchor's aria-describedby, except when the tooltip text equals the anchor's accessible name: then no aria-describedby and the tooltip is aria-hidden (owner decision M9 #2). A rich tooltip with an action is a non-modal role="dialog" labelled by its subhead (or the anchor; owner decision M9 #4): Tab from the anchor moves focus into it (Compose hasAction), but it does not trap Tab (DECISIONS D21 (1), WCAG 2.1.2; Compose's focusable popup wraps its focus search): Tab from its last control moves to the tab stop after the anchor (the tooltip's own controls skipped), Shift+Tab from its first returns to the anchor, Escape returns to the anchor and hides it, and focus leaving it hides it unless it is persistent (a persistent one stays until Escape, a click outside or hide()).

Every interactive part has a target of at least 48 × 48 px, a visible focus ring for keyboard focus only, and works under forced-colors and prefers-reduced-motion.

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §33 and the m3e-css sources.

PlainTooltipTokens.kt / RichTooltipTokens.kt@19991f2c plus the Tooltip.kt / internal/BasicTooltip.kt values Compose renders (components/tooltip-defaults.css). Plain: inverse surface, inverse-on-surface body small, extra-small corners, no elevation; 40 × 24dp minimum and 200dp maximum width with 8 × 4dp padding inside. Rich: surface container, medium corners, a Level2 shadow (tonal 0); up to 320dp wide, 16dp sides; the subhead (title small, on-surface-variant) has its first baseline 28dp from the top, the supporting text (body medium, on-surface-variant) its first baseline 24dp below the subhead (or the top) and 16dp under it, or 4dp above and below when it is alone; the action slot (label large, primary; the samples' TextButton) is at least 36dp with 8dp of bottom padding, 48dp + 8dp with the button's minimum interactive size. Caret (opt-in, caretShape defaults to null): TooltipDefaults.caretSize 16 × 8dp, on the edge facing the anchor (layoutCaret, caretX).

tooltip.css sources

(compose/material3 @ 19991f2c):

  • Tooltip.kt: PlainTooltip (Surface in TooltipDefaults' plain colors and shape, no elevation; sizeIn(TooltipMinWidth, plainTooltipMaxWidth, TooltipMinHeight) then PlainTooltipContentPadding; PlainTooltipTokens.SupportingTextFont), RichTooltip (Surface sizeIn(TooltipMinWidth, richTooltipMaxWidth, TooltipMinHeight) in richTooltipColors, RichTooltipTokens.ContainerShape, shadow RichTooltipTokens.ContainerElevation; a Column padded RichTooltipHorizontalPadding: the title paddingFromBaseline(HeightToSubheadFirstLine) in SubheadFont, the text in SupportingTextFont with textVerticalPadding, the action requiredHeightIn(ActionLabelMinHeight) padded ActionLabelBottomPadding in ActionLabelTextFont), TooltipBox (scale from 0.8 on FastSpatial, alpha from 0 on FastEffects, around the center), DefaultTooltipCaretShape and Modifier.layoutCaret.
  • TooltipSamples.kt: the rich tooltip's action is a TextButton (its minimum interactive size makes the action slot 48dp + the bottom padding tall).
  • paddingFromBaseline: a baseline sits --m3e-comp-tooltip-baseline-shift × the font size below the middle of its line box (Google Sans Flex metrics, as for the tabs).
  • Literals: components/tooltip-defaults.css (PARITY.md).