A small floating label with an arrow, shown on hover or focus to name or explain the thing under it.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
arrowsmbk-tooltip--arrow-<value> | top-starttop-centertop-endbottom-startbottom-centerbottom-endleftright | top-center | Which edge of the bubble the arrow sits on, and where along it: top points up at a target below the bubble, bottom points down at a target above it. Beside a target, the side it prefers: it takes the opposite side when this one has no room, and the arrow turns with it. Figma calls this "Arrow" and uses the same names. |
inversesmbk-tooltip--inverse | On or off | Off | Dark surface with inverse text, for use over light content. |
Examples
Usage
An icon button named in a tooltip: rest the pointer on it a moment, or move the keyboard focus to it for it at once, and it shows under it; Escape or moving on hides it.
Two sentences
A short explanation wraps at the greatest width. It prefers the side over the button and takes the other side when there is no room.
Arrows
Eight arrow positions, each pointing at a target on that side.
Inverse
The dark surface, for use over light content, as the tooltips beside controls use.
With a key
A control’s key after its name, as a small key cap, on either surface. The control carries the key for screen readers; the page listens for it.
Guidelines
Purpose
A small label that appears beside a control while the reader points at it or moves the keyboard focus to it, to say what it is or add a short explanation, and goes away when they move on. For an icon-only control it is the control's name made visible. It holds a few words, or a sentence or two, and nothing to press; for more, use a popover.
When to use
- Naming an icon-only control.
- A short hint or a sentence of explanation that would clutter the interface if always shown.
When not to use
- Content that must be read to use the page. Put it on the page.
- Anything to press or fill in: a tooltip cannot be clicked into. Use a popover.
- A paragraph, a list or several steps. Use a popover, or put it on the page.
- The only place for the words: a touch screen cannot point, so the same words must be found elsewhere, such as the control's name.
Usage rules
Do
- Keep it to a few words, or a sentence or two.
- Use it to name a control that has no visible label.
Don't
- Repeat the visible label of the control in the tooltip.
- Put a link, a button or a field in it.
Keep a tooltip to a few words, or a sentence or two that explain the control, so it can be read in passing.
A paragraph of help in a tooltip covers what is around it and is gone when the pointer moves; put it on the page or in a popover.
Use a tooltip to name a control that has no visible label, so it adds something.
A tooltip that repeats the button's own label adds nothing and gets in the way.
Anatomy
- The block, holding the bubble and the arrow.
- The bubble part, a small box holding the words. It is as wide as its words up to the tooltip's greatest width, where they wrap onto more lines.
- The key part, optional: the target's key after the words, drawn as a small key cap ("Zoom in" then "]"), given as
shortcut. The bubble keeps its one-line height. - The arrow part, a small triangle on one edge pointing at the target. The arrow axis names its edge and where along it: a top arrow points up at a target below the bubble's top, so the bubble sits under the target; a bottom arrow sits the bubble over the target; a left arrow sits it to the target's right; a right arrow to its left.
- With a target: the anchor part holds the target and the layer part, and the layer holds the tooltip.
Behavior
Given its target as trigger, the tooltip draws the target and shows itself: once a mouse has rested on the target a moment, long enough that a pointer merely passing over a row of controls shows nothing, and at once when the target takes the keyboard focus. A label's help waits less, since pointing at its icon is asking for it. It stays while the pointer moves from the target onto the tooltip and while the focus stays, so it can be read at leisure. It goes when the pointer leaves both, when the focus moves on, when the target is pressed, and when Escape is pressed, which leaves the focus where it is. Once one tooltip has shown, the next one shows at once as the pointer moves along a row of controls. It never shows from a touch.
It is placed beside its target on the side its arrow names, lifted over the page so no scrolling or clipped part around the target cuts it off. Without room on that side it takes the opposite side, and its arrow turns with it. Near the window's edge it moves inward, its arrow still pointing at the target's middle. It stays beside the target as the page scrolls, and closes when the target scrolls out of the window. It can be shown from the start, until the reader moves on.
Given a shortcut, the tooltip shows the target's key after its words, and the target carries the key as aria-keyshortcuts. The tooltip only shows the key: the page listens for it, since only the page knows when a key is safe to take, such as a single letter outside text fields.
Without script, a shown tooltip hangs beside its target on the side its arrow names, so markup written by hand can show and hide it with the hidden attribute. Without a target, the component draws the bubble alone, shown, for a page that shows and places it itself.
It fades in quickly when it appears, coming a short way out of its target on the side it was placed, and fades out where it is when it is hidden; under reduced motion it fades in and out without traveling.
Accessibility
What it does for you
- The bubble carries the tooltip role and holds text only, never anything focusable.
- When its words are the target's own name, as on an icon button, they are the target's accessible name and the bubble is hidden from assistive technology, so the name is not read twice.
- When its words add to the name, the target is described by the bubble, so its words are read after the name.
- A key cap in the bubble is hidden from assistive technology; the target carries the key as
aria-keyshortcuts, where a screen reader looks for it. - It shows on keyboard focus as well as on pointing, stays while pointed at, and Escape dismisses it, leaving focus where it is.
What you must do
- Never make a tooltip the only place for its words: a touch screen cannot point and it never shows from a touch, so the same words must be found elsewhere, such as the control's name.
- Put content that must be read to use the page on the page, and anything to press or fill in in a popover.
How to test
- Tab to the target: the tooltip shows at once, and the target keeps its focus ring. Press Escape: the tooltip goes and focus stays where it was.
- With a screen reader, an icon-only target is read by its name once, and a target whose tooltip adds to its name is read with the tooltip's words after the name.
- Point at the target with a mouse, then move onto the tooltip: it stays, so it can be read at leisure.
- On a touch screen, check that the same words are found elsewhere, since the tooltip does not show from a touch.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the target, which shows the tooltip at once. |
| Escape | Dismisses the tooltip and leaves focus where it is. |
Source
The files the tooltip is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-tooltip — Tooltip. Figma "Tooltip" 4170:13949 and "TooltipItem" 4170:13718.
* The bubble is the item: it hugs its words and wraps at the tooltip's greatest
* width. The arrow is a small shape cut from a box the same color, placed on one
* edge. With a target, the anchor holds the target and the layer, and the wrapper
* shows the tooltip and places it beside the target; on its own, the page places it.
* It fades in as it appears and out as it is hidden (motion rules at the end). */
.smbk-tooltip {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
color: var(--smbk-color-text-default);
}
/* The words set its size: the extra-small inset over and under them, the medium inset at their sides, inside the
hairline edge, as Figma counts the edge in its 28 tall one-line bubble. As wide as the words, up to the tooltip's
greatest width, where they wrap, whatever the part it sits in says about wrapping and alignment. */
.smbk-tooltip__bubble {
box-sizing: border-box;
display: block;
width: max-content;
max-width: var(--smbk-size-tooltip-max-width);
padding: var(--smbk-space-inset-xs) var(--smbk-space-inset-md);
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-rim);
border-radius: var(--smbk-radius-control-sm);
background: var(--smbk-color-surface-raised);
box-shadow: var(--smbk-elevation-overlay);
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
text-align: start;
white-space: normal;
overflow-wrap: break-word;
}
/* A control's key after its words, as a small key cap ("Zoom in ]"): the words and the cap in a row, the small
inline gap apart, the cap centred on the words' line, which is taller than the cap, so the bubble keeps its
one-line height in every browser. */
.smbk-tooltip__bubble:has(> .smbk-tooltip__key) {
display: flex;
align-items: center;
gap: var(--smbk-space-inline-sm);
}
.smbk-tooltip__key {
flex: none;
}
/* The arrow reaches a hairline into the bubble, so it lies over the bubble's edge where the two
meet and they read as one shape, as Figma draws it (its 12 by 5 arrow overlaps the bubble's
inside stroke by one); it comes after the bubble and is positioned, so it paints over the
edge. Its depth is half its width less that hairline. Each direction is its own triangle. */
.smbk-tooltip__arrow {
position: absolute;
width: var(--smbk-size-icon-xs);
height: calc(var(--smbk-size-icon-xs) / 2 - var(--smbk-border-width-hairline));
background: var(--smbk-color-surface-raised);
}
/* ---- arrow placement: the arrow sits on the named edge, as Figma's Arrow Position names it:
top points up at a target below the bubble, bottom points down at a target above it. A
centered arrow takes the middle from the tooltip's own centering. ---- */
.smbk-tooltip--arrow-top-start .smbk-tooltip__arrow,
.smbk-tooltip--arrow-top-center .smbk-tooltip__arrow,
.smbk-tooltip--arrow-top-end .smbk-tooltip__arrow {
bottom: calc(100% - var(--smbk-border-width-hairline));
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.smbk-tooltip--arrow-bottom-start .smbk-tooltip__arrow,
.smbk-tooltip--arrow-bottom-center .smbk-tooltip__arrow,
.smbk-tooltip--arrow-bottom-end .smbk-tooltip__arrow {
top: calc(100% - var(--smbk-border-width-hairline));
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
.smbk-tooltip--arrow-top-start .smbk-tooltip__arrow,
.smbk-tooltip--arrow-bottom-start .smbk-tooltip__arrow {
left: var(--smbk-space-inset-md);
}
.smbk-tooltip--arrow-top-end .smbk-tooltip__arrow,
.smbk-tooltip--arrow-bottom-end .smbk-tooltip__arrow {
right: var(--smbk-space-inset-md);
}
.smbk-tooltip--arrow-left .smbk-tooltip__arrow,
.smbk-tooltip--arrow-right .smbk-tooltip__arrow {
top: 50%;
width: calc(var(--smbk-size-icon-xs) / 2 - var(--smbk-border-width-hairline));
height: var(--smbk-size-icon-xs);
translate: 0 -50%;
}
.smbk-tooltip--arrow-left .smbk-tooltip__arrow {
right: calc(100% - var(--smbk-border-width-hairline));
clip-path: polygon(0 50%, 100% 0, 100% 100%);
}
.smbk-tooltip--arrow-right .smbk-tooltip__arrow {
left: calc(100% - var(--smbk-border-width-hairline));
clip-path: polygon(100% 50%, 0 0, 0 100%);
}
/* Placed beside its target, the wrapper sets the arrow's middle where the target's middle is (an inline offset
along the edge, kept the medium inset from the corners), so it still points at the target when the bubble has
moved inward at the window's edge. */
.smbk-tooltip[data-placed]:is(.smbk-tooltip--arrow-top-start, .smbk-tooltip--arrow-top-center, .smbk-tooltip--arrow-top-end, .smbk-tooltip--arrow-bottom-start, .smbk-tooltip--arrow-bottom-center, .smbk-tooltip--arrow-bottom-end) .smbk-tooltip__arrow {
right: auto;
translate: -50% 0;
}
/* ---- inverse ---- */
.smbk-tooltip--inverse {
color: var(--smbk-color-text-inverse);
}
.smbk-tooltip--inverse .smbk-tooltip__bubble {
background: var(--smbk-color-surface-inverse);
border-color: var(--smbk-color-border-inverse);
}
.smbk-tooltip--inverse .smbk-tooltip__arrow {
background: var(--smbk-color-surface-inverse);
}
/* ---- with a target: the anchor holds the target and the layer, the layer the tooltip ---- */
.smbk-tooltip__anchor {
position: relative;
display: inline-flex;
}
/* Until the wrapper lifts it, the layer has no box: the tooltip hangs from the anchor on the side its arrow names, the
extra-small stack step from the target past the arrow's tip (the arrow reaches half its width less two hairlines
out of the bubble), as in markup written by hand and before the page's
script runs. A top arrow hangs it below the target, a bottom arrow above, a left arrow to its right and a right
arrow to its left; start and end line its edge up with the target's, center its middle. The layer, its lift into
the top layer and the closed tooltip are the floating panels' own, shared with the dropdown and the popover:
floating/floating.css. */
.smbk-tooltip__layer > .smbk-tooltip {
position: absolute;
z-index: var(--smbk-layer-overlay);
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-top-start, .smbk-tooltip--arrow-top-center, .smbk-tooltip--arrow-top-end) {
top: calc(100% + var(--smbk-space-stack-xs) + var(--smbk-size-icon-xs) / 2 - 2 * var(--smbk-border-width-hairline));
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-bottom-start, .smbk-tooltip--arrow-bottom-center, .smbk-tooltip--arrow-bottom-end) {
bottom: calc(100% + var(--smbk-space-stack-xs) + var(--smbk-size-icon-xs) / 2 - 2 * var(--smbk-border-width-hairline));
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-top-start, .smbk-tooltip--arrow-bottom-start) {
left: 0;
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-top-end, .smbk-tooltip--arrow-bottom-end) {
right: 0;
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-top-center, .smbk-tooltip--arrow-bottom-center) {
left: 50%;
translate: -50% 0;
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-left, .smbk-tooltip--arrow-right) {
top: 50%;
translate: 0 -50%;
}
.smbk-tooltip__layer > .smbk-tooltip--arrow-left {
left: calc(100% + var(--smbk-space-stack-xs) + var(--smbk-size-icon-xs) / 2 - 2 * var(--smbk-border-width-hairline));
}
.smbk-tooltip__layer > .smbk-tooltip--arrow-right {
right: calc(100% + var(--smbk-space-stack-xs) + var(--smbk-size-icon-xs) / 2 - 2 * var(--smbk-border-width-hairline));
}
/* Placed by the wrapper, it takes only the place the wrapper writes. */
.smbk-tooltip__layer > .smbk-tooltip[data-placed] {
inset: auto;
translate: none;
}
/* Lifted into the top layer while the tooltip shows, it takes the place the wrapper gives it, and the pointer can move
onto it and it stays (the wrapper keeps it open while pointed at; the press it takes is floating.css's). */
.smbk-tooltip__layer:popover-open > .smbk-tooltip {
translate: none;
}
/* ---- motion: a tooltip is feedback to pointing at or focusing its target, so it fades in with
the feedback purpose when it appears and fades out the same way when the page hides it with
the hidden attribute. Shown beside a target, it comes out of the target as it fades in, from the
extra-small stack step nearer it, on the side it was placed (its arrow names it), and fades out where it is.
Moving from one tooltip straight to the next, the wrapper marks the new one data-instant and it shows at
once. No movement under reduced motion. ---- */
.smbk-tooltip {
transition-property: opacity, display;
transition-duration: var(--smbk-motion-feedback-duration);
transition-timing-function: var(--smbk-motion-feedback-easing);
transition-behavior: allow-discrete;
}
/* Hidden, it is gone (floating.css) and faded out. */
.smbk-tooltip[hidden] {
opacity: 0;
}
@starting-style {
.smbk-tooltip {
opacity: 0;
}
}
/* The coming out of the target is an animation named for the side, rather than a starting style: the side is
known only once the tooltip shows and has been placed, and a new name starts the animation again from that side,
before anything is drawn. */
@media (prefers-reduced-motion: no-preference) {
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-top-start, .smbk-tooltip--arrow-top-center, .smbk-tooltip--arrow-top-end) {
animation: smbk-tooltip-drop var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing);
}
.smbk-tooltip__layer > :is(.smbk-tooltip--arrow-bottom-start, .smbk-tooltip--arrow-bottom-center, .smbk-tooltip--arrow-bottom-end) {
animation: smbk-tooltip-rise var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing);
}
.smbk-tooltip__layer > .smbk-tooltip--arrow-left {
animation: smbk-tooltip-slide-right var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing);
}
.smbk-tooltip__layer > .smbk-tooltip--arrow-right {
animation: smbk-tooltip-slide-left var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing);
}
.smbk-tooltip__layer > .smbk-tooltip[data-instant] {
animation: none;
}
}
@keyframes smbk-tooltip-drop {
from {
opacity: 0;
transform: translateY(calc(-1 * var(--smbk-space-stack-xs)));
}
}
@keyframes smbk-tooltip-rise {
from {
opacity: 0;
transform: translateY(var(--smbk-space-stack-xs));
}
}
@keyframes smbk-tooltip-slide-right {
from {
opacity: 0;
transform: translateX(calc(-1 * var(--smbk-space-stack-xs)));
}
}
@keyframes smbk-tooltip-slide-left {
from {
opacity: 0;
transform: translateX(var(--smbk-space-stack-xs));
}
}
.smbk-tooltip[data-instant] {
transition: none;
}
/* Under reduced motion the tooltip still fades in and out with the feedback purpose: only its
opacity and its display change over time, so nothing it might travel moves. */
@media (prefers-reduced-motion: reduce) {
.smbk-tooltip {
transition-property: opacity, display;
}
}
/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
user's own colors. Nothing here applies outside that mode. */
@media (forced-colors: active) {
/* The arrow is a background, which forced colors drops; drawn in the edge's color it points
again and closes the gap it cut in the edge. */
.smbk-tooltip__arrow,
.smbk-tooltip--inverse .smbk-tooltip__arrow {
background: CanvasText;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| size.tooltip.max-width--smbk-size-tooltip-max-width | 240px | Size | |
| space.inset.xs--smbk-space-inset-xs | 4px | Spacing | |
| space.inset.md--smbk-space-inset-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.rim--smbk-color-border-rim | light: #ffffffe6dark: #ffffff0dchanges with theme | Color | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| color.surface.raised--smbk-color-surface-raised | light: #f9fafbdark: #232527changes with theme | Color | |
| elevation.overlay--smbk-elevation-overlay | light: 0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326dark: 0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033 | Elevation | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| size.icon.xs--smbk-size-icon-xs | 12px | Size | |
| color.text.inverse--smbk-color-text-inverse | light: #f9fafbdark: #232527changes with theme | Color | |
| color.surface.inverse--smbk-color-surface-inverse | light: #2d3237dark: #e7ebf0changes with theme | Color | |
| color.border.inverse--smbk-color-border-inverse | light: #232527dark: #f9fafbchanges with theme | Color | |
| layer.overlay--smbk-layer-overlay | 1200 | Stacking order | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| motion.feedback.duration--smbk-motion-feedback-duration | 150ms | Motion | |
| motion.feedback.easing--smbk-motion-feedback-easing | cubic-bezier(0.16, 1, 0.3, 1) | Motion |
Manifest
{
"name": "tooltip",
"block": "smbk-tooltip",
"summary": "A small floating label with an arrow, shown on hover or focus to name or explain the thing under it.",
"adapt": "css",
"axes": {
"arrow": {
"values": [
"top-start",
"top-center",
"top-end",
"bottom-start",
"bottom-center",
"bottom-end",
"left",
"right"
],
"default": "top-center",
"description": "Which edge of the bubble the arrow sits on, and where along it: top points up at a target below the bubble, bottom points down at a target above it. Beside a target, the side it prefers: it takes the opposite side when this one has no room, and the arrow turns with it. Figma calls this \"Arrow\" and uses the same names."
}
},
"flags": {
"inverse": {
"description": "Dark surface with inverse text, for use over light content."
}
},
"parts": [
"bubble",
"key",
"arrow",
"anchor",
"layer"
],
"figmaNodeId": "4170:13949"
}