|
| 1 | +# EUI tooltip content: no interactive elements |
| 2 | + |
| 3 | +**Applies to:** `EuiToolTip`, `EuiIconTip` — the `content` and `title` props |
| 4 | + |
| 5 | +Tooltip `content` and `title` render inside a portal with `role="tooltip"`. The overlay only appears while the trigger is hovered or focused and is dismissed on blur, so any focusable element placed inside it is unreachable by keyboard and assistive-technology users. Use **`EuiPopover`** when the content needs to be interactive. |
| 6 | + |
| 7 | +**Related guides:** **`overlays.md`** (`EuiPopover` for interactive content) · **`tooltip_icon.md`** (wrapping `EuiButtonIcon` with `EuiToolTip`) · **`icons_and_tooltips.md`** (`EuiIconTip` vs `EuiToolTip` + `EuiIcon`). |
| 8 | + |
| 9 | +## Canonical usage |
| 10 | + |
| 11 | +- `EuiToolTip` / `EuiIconTip` `content` and `title` may contain: |
| 12 | + - **Plain strings** (preferred — easy to localize and read). |
| 13 | + - **Non-interactive JSX** — text nodes, `<span>`, `<p>`, `EuiText`, `EuiIcon`, and the display-only badges/cards (`EuiBadge`, `EuiBetaBadge`, `EuiCard`) used **without** `onClick` / `href`. |
| 14 | +- They must **not** contain anything focusable: |
| 15 | + - Native `<a>`, `<button>`, `<input>`, `<select>`, `<textarea>`. |
| 16 | + - Interactive EUI components — `EuiLink`, `EuiButton`, `EuiButtonEmpty`, `EuiButtonIcon`, `EuiFieldText`, `EuiFieldNumber`, `EuiFieldSearch`, `EuiFieldPassword`, `EuiTextArea`, `EuiSelect`, `EuiSuperSelect`, `EuiComboBox`, `EuiSelectable`, `EuiSwitch`, `EuiCheckbox`, `EuiRadio`, `EuiRange`, `EuiDualRange`, `EuiColorPicker`, `EuiDatePicker`, `EuiSuperDatePicker`, `EuiFilterButton`, `EuiPagination`, `EuiTab`, `EuiTreeView`, `EuiContextMenuItem`, `EuiKeyPadMenuItem`, `EuiListGroupItem`, `EuiBreadcrumbs`, `EuiBasicTable`, `EuiInMemoryTable`, `EuiCheckableCard`, … |
| 17 | +- The rule searches recursively — interactive elements nested inside fragments, conditional renders (`cond && …`, `cond ? … : …`), or wrapper elements are reported too. |
| 18 | +- When users need to interact with the content (click a link, fill a field), switch the wrapper to **`EuiPopover`** triggered by an explicit click — never by hover. |
| 19 | + |
| 20 | +### Manual-review cases (rule is silent) |
| 21 | + |
| 22 | +- **Variable content** — `content={tooltipContent}` / `title={titleNode}` is intentionally skipped because it cannot be statically analyzed. Trace the variable and verify it never holds focusable JSX. |
| 23 | +- **Conditionally-interactive components** — `EuiBadge`, `EuiBetaBadge`, `EuiCard` are excluded from the rule because they render as a plain element without `onClick` / `href`. As soon as you add `onClick` or `href`, they become focusable and the same restriction applies — move the interaction out of the tooltip. |
| 24 | + |
| 25 | +## Examples |
| 26 | + |
| 27 | +```tsx |
| 28 | +<EuiToolTip content="Just text"> |
| 29 | + <EuiButton>Hover me</EuiButton> |
| 30 | +</EuiToolTip> |
| 31 | + |
| 32 | +<EuiToolTip content={<EuiText><p>Description</p></EuiText>}> |
| 33 | + <EuiButton>Hover me</EuiButton> |
| 34 | +</EuiToolTip> |
| 35 | + |
| 36 | +<EuiIconTip content="Informational text" type="info" /> |
| 37 | + |
| 38 | +// Display-only badge is fine |
| 39 | +<EuiToolTip content={<EuiBadge>v2.0</EuiBadge>}> |
| 40 | + <EuiButton>Hover me</EuiButton> |
| 41 | +</EuiToolTip> |
| 42 | +``` |
| 43 | + |
| 44 | +## Common mistakes |
| 45 | + |
| 46 | +```tsx |
| 47 | +// WRONG — link inside tooltip is not keyboard-reachable |
| 48 | +<EuiToolTip content={<EuiLink href="/docs">Learn more</EuiLink>}> |
| 49 | + <EuiButton>Hover me</EuiButton> |
| 50 | +</EuiToolTip> |
| 51 | + |
| 52 | +// RIGHT — switch to EuiPopover so the link participates in the focus order |
| 53 | +const [isOpen, setIsOpen] = useState(false); |
| 54 | +const togglePopover = () => setIsOpen((open) => !open); |
| 55 | +const closePopover = () => setIsOpen(false); |
| 56 | + |
| 57 | +<EuiPopover |
| 58 | + button={<EuiButton onClick={togglePopover}>More info</EuiButton>} |
| 59 | + isOpen={isOpen} |
| 60 | + closePopover={closePopover} |
| 61 | +> |
| 62 | + <EuiLink href="/docs">Learn more</EuiLink> |
| 63 | +</EuiPopover> |
| 64 | + |
| 65 | +// WRONG — button inside `EuiIconTip` content |
| 66 | +<EuiIconTip content={<EuiButton>Click</EuiButton>} type="info" /> |
| 67 | + |
| 68 | +// RIGHT — keep the icon tip purely informational |
| 69 | +<EuiIconTip content="Informational text" type="info" /> |
| 70 | + |
| 71 | +// WRONG — interactive element inside `title` is also reported |
| 72 | +<EuiToolTip title={<EuiLink href="#">Learn more</EuiLink>} content="Info"> |
| 73 | + <EuiButton>Hover</EuiButton> |
| 74 | +</EuiToolTip> |
| 75 | + |
| 76 | +// WRONG — interactive child wrapped in a fragment is reported recursively |
| 77 | +<EuiToolTip content={<><span>Text</span><EuiLink href="#">Link</EuiLink></>}> |
| 78 | + <EuiButton>Hover</EuiButton> |
| 79 | +</EuiToolTip> |
| 80 | + |
| 81 | +// WRONG — interactive child behind `cond && …` is reported |
| 82 | +<EuiToolTip content={<span>{cond && <EuiLink href="#">Link</EuiLink>}</span>}> |
| 83 | + <EuiButton>Hover</EuiButton> |
| 84 | +</EuiToolTip> |
| 85 | + |
| 86 | +// WRONG — interactive child inside a ternary is reported |
| 87 | +<EuiToolTip content={cond ? <EuiLink href="#">Link</EuiLink> : null}> |
| 88 | + <EuiButton>Hover</EuiButton> |
| 89 | +</EuiToolTip> |
| 90 | + |
| 91 | +// WRONG — conditionally-interactive badge with onClick becomes focusable |
| 92 | +<EuiToolTip content={<EuiBadge onClick={onClick}>Open</EuiBadge>}> |
| 93 | + <EuiButton>Hover</EuiButton> |
| 94 | +</EuiToolTip> |
| 95 | +``` |
0 commit comments