Communication
Tooltips
Plain tooltips name a control; rich tooltips explain it, optionally with an action.
- COMPONENT_SPEC §33
- CSS · custom elements · React
- Open in the playground
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.
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 getsaria-describedby, unless the tooltip text equals its accessible name: the tooltip is thenaria-hidden);anchorElementsets it directly.variant:plain(default) |rich. A rich tooltip takessubhead(its title) andaction(the label of a text button; clicking it firesactionand, unless that is cancelled, hides the tooltip). A tooltip with an action is a non-modaldialognamed by its subhead (or the anchor) that Tab from the anchor moves into.position:above(default) |below|left|right|start|end(TooltipAnchorPosition).caretdraws TooltipDefaults.caretShape().persistent: TooltipState.isPersistent — stays until a click outside, Escape orhide()(leaving the anchor does not hide it, nor does the duration).manual: no hover / focus / long press handling (TooltipBox enableUserInput = false); onlyopen,show()andhide()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-changewithdetail: { 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).