# Tooltip

A small floating label with an arrow, shown on hover or focus to name or explain the thing under it.

Block: `smbk-tooltip`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `arrow` | `top-start`, `top-center`, `top-end`, `bottom-start`, `bottom-center`, `bottom-end`, `left`, `right` | `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. |
| `inverse` | on / off | off | Dark surface with inverse text, for use over light content. |

Parts: `smbk-tooltip__bubble`, `smbk-tooltip__key`, `smbk-tooltip__arrow`, `smbk-tooltip__anchor`, `smbk-tooltip__layer`

## Markup

```html
<span role="tooltip" class="smbk-tooltip smbk-tooltip--arrow-top-center">
  <span class="smbk-tooltip__bubble">Copy link</span>
  <span class="smbk-tooltip__arrow" aria-hidden="true"></span>
</span>
```

## React

```tsx
import { Tooltip } from './design-system/components/tooltip/tooltip.js';

<Tooltip arrow="top-center">Copy link</Tooltip>
```

## Stylesheet

```css
/* 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

```json
{
  "color": {
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "inverse": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "Text on the opposite surface."
      }
    },
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "inverse": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Edge on the opposite surface."
      }
    },
    "surface": {
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      },
      "inverse": {
        "$type": "color",
        "$value": "{{color.grey.850}}",
        "$description": "The opposite surface: tooltips, dark chips."
      }
    }
  },
  "size": {
    "tooltip": {
      "max-width": {
        "$type": "dimension",
        "$value": "{{size.width.240}}",
        "$description": "Placeholder:. The widest a tooltip bubble grows before its text wraps. Points at size/width/240 for now."
      }
    },
    "icon": {
      "xs": {
        "$type": "dimension",
        "$value": "{{size.12}}",
        "$description": "Icon box, extra small."
      }
    }
  },
  "space": {
    "inset": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    },
    "inline": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    },
    "stack": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things stacked one above another, extra small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "radius": {
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "elevation": {
    "overlay": {
      "$type": "shadow",
      "$value": {
        "light": "0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326",
        "dark": "0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033"
      }
    }
  },
  "typography": {
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "layer": {
    "overlay": {
      "$type": "number",
      "$value": 1200
    }
  },
  "motion": {
    "feedback": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.150}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    }
  }
}
```

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

## 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](kbd.md) ("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.

## 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](popover.md).
- 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.

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

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