Tooltip
What this file covers (quick):
- How to use the
Tooltipcomponent - Props & defaults
- Accessibility
- Short implementation caveats
Dependenciesβ
Basic usageβ
import Tooltip from '@components/Tooltip';
export default function Example() {
return (
<Tooltip content="Helpful hint">
<button>Hover or focus me</button>
</Tooltip>
);
}
The Tooltip will attach the attributes data-tooltip-id and data-tooltip-content to the element you pass as children when possible.
If you pass a non-element child (plain text or multiple nodes), the component wraps them in a focusable <span tabIndex={0}>
so keyboard users can discover the tooltip. The actual tooltip element is rendered by react-tooltip and appended to document.body as a portal.
Propsβ
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
content | string | Yes | Text shown inside the tooltip. Keep it short - tooltips are for hints. | |
children | node | Yes | Element that triggers the tooltip. Prefer a single React element (<button>, <a>, <Link>, etc.). If a non-element is passed (string / fragment) the component wraps it in a focusable <span>. | |
id | string | No | generated | Optional ID to control multiple tooltips. |
dynamicPositioning | bool | No | true | When true, fallback placements ['bottom','top','left'] will be tried if the preferred placement (place="right") doesn't fit. When false no fallbacks are provided. |
children remains typed as node so you can pass text, small fragments or an element,
but the component behaves best when given a single element so attributes can be attached directly.
Accessibility & Link behaviourβ
react-tooltiprenders a node withrole="tooltip"; screen-readers can discover the tooltip content through that node.- Keyboard support: the component enables focus-triggered tooltips using
openOnFocus. For the tooltip to open on keyboard navigation, the element that receives focus must be the same element the tooltip is attached to:- If you pass a focusable element such as
<button>,<a>, or a<Link>component,Tooltipwill attach the necessary attributes to that element andopenOnFocuswill work as expected. - If you pass plain text or a non-focusable element,
Tooltipwill wrap it in a<span tabIndex={0}>so it becomes keyboard focusable.
- If you pass a focusable element such as
- Important: Do not wrap a focusable child inside an extra
tabIndex={0}element - this creates two tab stops (double focus). Prefer giving the tooltip attributes directly to the interactive element. For example, wrap the<a>withTooltiprather than puttingTooltipinside the<a>with a nested focusable wrapper.
Good: attach tooltip to the interactive elementβ
<Tooltip content="Open project on GitHub (opens in new tab)">
<a href="https://github.com/..." target="_blank" rel="noopener noreferrer">
GitHub
</a>
</Tooltip>
Bad: wrapping the interactive element inside a focusable wrapper (creates duplicate focus targets)β
/* avoid this */
<a href="..." target="_blank" rel="noopener noreferrer">
<Tooltip content="...">
<span>GitHub</span> {/* the span might be focusable and compete with the link */}
</Tooltip>
</a>
Implementation notes (what the component does)β
- The component tries to attach
data-tooltip-idanddata-tooltip-contentdirectly to the single React child you pass by cloning it. This preserves semantics for<a>,<button>and<Link>components and avoids double tab stops. Ifchildrenis not a valid single element, the component renders a<span tabIndex={0}>wrapper and attaches the attributes there. appendTo={document.body}andpositionStrategy="fixed"- the tooltip is rendered as a portal to the document body so it sits above layout and isnβt clipped by scroll/overflow.useId()is used to generate a stable id at runtime; you can pass your ownidprop if you need deterministic IDs.dynamicPositioningdefaulttrueprovides fallback placements when the preferred placement doesn't fit. Set tofalseto force a single placement.openOnFocusis enabled so keyboard users can open the tooltip when the trigger element receives focus. Make sure the trigger element is focusable (native element or wrapper withtabIndex={0}).- Styling & animations:
react-tooltipadds classes and may apply show/hide transitions. If you change global CSS or reset transitions you may affect tooltip visibility timing and tests.
Examplesβ
<Tooltip content="Do the thing">
<button type="button">Action</button>
</Tooltip>
<Tooltip content="Go to profile">
<Link to="/profile">Profile</Link>
</Tooltip>
<Tooltip content="Open on GitHub (opens in a new tab)">
<a href="https://github.com/..." target="_blank" rel="noopener noreferrer">
GitHub
</a>
</Tooltip>
<Tooltip content="Short hint">Some inline text or an icon-only element</Tooltip>
Testing tipsβ
-
react-tooltipmounts the tooltip node intodocument.body. In thejsdomenvironmentscreenqueries will still find it. -
Portal timing & animations can make tests flaky. Wrap assertions in
await waitFor()or usefindBy*queries which retry until the element appears. Example patterns:Hoverawait user.hover(screen.getByText('Hover me'));
expect(await screen.findByText('Hello tooltip')).toBeVisible();Focusawait user.tab();
expect(await screen.findByText('Hello tooltip')).toBeVisible();Hide with waitFor to accommodate transitionawait waitFor(() => expect(screen.queryByText('Hello tooltip')).not.toBeInTheDocument()); -
In tests prefer passing an actual element as
children(button, Link, or anchor) so the library attributes are attached directly and keyboard focus works reliably. If you need to assert behavior for plain text triggers, test the wrapped<span>behavior explicitly. -
If you see intermittent failures due to CSS transitions, disable transitions in your test setup (for example, add a small global CSS rule to turn off transitions during tests) - this makes timing deterministic.
Summaryβ
- The
Tooltipprefers to attach attributes directly to a single React child (preserves semantics for<a>,<button>,<Link>). - If you pass non-element children, the component wraps them in a focusable
<span>so keyboard users can reveal the tooltip. - For external links (anchors), wrap the anchor with
Tooltip(so tooltip attributes are attached to the anchor) - do not make an extra focusable wrapper inside the anchor. openOnFocus+ focusable trigger = keyboard-accessible tooltip.