An accessible React tooltip appears when the user hovers over or focuses a control, and disappears on Escape or when focus leaves. The tooltip itself never takes focus, and a screen reader hears it through the trigger's description.
The state and the id
The tooltip needs two things: a boolean for whether it is showing, and a unique id so the trigger can reference the popup. The useId hook supplies the id.
import { useId, useState } from "react";
const id = useId();
const [open, setOpen] = useState(false);The id is stable across renders and unique per component instance, so several tooltips on a page never collide. The open state is the single source of truth for visibility.
The trigger and the popup
The trigger is a button that shows and hides the tooltip through mouse and keyboard events, and the popup renders only while open.
function Tooltip({ label, children }) {
const id = useId();
const [open, setOpen] = useState(false);
return (
<button className="relative" aria-describedby={open ? id : undefined}
onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)} onBlur={() => setOpen(false)}
onKeyDown={(event) => { if (event.key === "Escape") setOpen(false); }}>
{children}
{open && <span role="tooltip" id={id}>{label}</span>}
</button>
);
}The aria-describedby attribute points the trigger at the tooltip only while it is open, so screen readers announce the label at the right moment. onFocus and onBlur cover keyboard users, onMouseEnter and onMouseLeave cover pointer users, and Escape dismisses the tooltip while focus stays on the button.
Position it with CSS
The popup needs to float above the trigger. A small absolute positioning rule places it just above the button.
.tooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}The class keeps the tooltip above the trigger and centered. Because the button is relatively positioned, the absolutely positioned popup anchors to it rather than the page. Keep the tooltip close to its trigger and avoid letting it cover the control it describes, so the relationship between them stays obvious.
Keep tooltips non-interactive
A tooltip should never contain buttons or links. Focus stays on the trigger, so interactive content inside a tooltip is unreachable for keyboard users and should move into a dialog instead.
The WAI-ARIA tooltip pattern is still marked as a work in progress, so its core contract stays small: role tooltip on the popup and aria-describedby on the trigger. Everything else is the hover, focus, and Escape behavior this component already adds, and a small hover delay keeps tooltips from flickering during casual mouse movement.
Why this beats the title attribute
The native title attribute does none of this. It ignores keyboard focus, cannot be styled, and appears on its own schedule. A real tooltip is just a small state machine around the semantics described in ARIA in React.
When several widgets need this behavior, rebuilding each one stops being worth it. The headless UI components approach packages exactly this logic so you never hand write it twice.
Rune AI
Key Insights
- Show the tooltip on mouse hover and keyboard focus.
- Give the popup role tooltip and a unique id.
- Point the trigger at it with aria-describedby.
- Close on Escape, blur, and mouse leave.
- Keep focus on the trigger, never inside the tooltip.
Frequently Asked Questions
Why not just use the title attribute?
Does the tooltip itself receive focus?
How do I avoid two tooltips sharing an id?
Conclusion
An accessible tooltip is a small popup that appears on hover and focus, names itself with role tooltip, links to its trigger with aria-describedby, and closes on Escape or when focus leaves. A few lines of React state wire the whole thing.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.