A tooltip is a short hint that appears while its trigger is hovered or focused. Build the React version with one boolean that controls the bubble and its accessible relationship to the button.
Implement the default App component in App.tsx; it receives no props and renders the complete trigger and tooltip.
Hover me displays Saved to your library; moving the pointer away hides it.aria-describedby="library-tip" and the bubble has id="library-tip" with role="tooltip".useState(false).onMouseEnter/onMouseLeave with onFocus/onBlur.show is true.aria-describedby only while its matching tooltip exists.styles.css.Keep visibility in one React state value and derive both the bubble and its accessible description link from it.
A tooltip must respond to pointer and keyboard attention. Showing a bubble is only half the job: while it exists, the trigger should identify it as a description.
show is the single source of truth. Pointer and focus events update it, then React uses the same boolean to render #library-tip and set aria-describedby.
const [show, setShow] = useState(false);
<button onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
Hover me
</button>
This works with a mouse, but keyboard focus never changes show. It also omits the relationship that lets assistive technology treat the bubble as the button's description.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [show, setShow] = useState(false);
return (
<main className="container">
<h1>Tooltip</h1>
<div className="stage">
<span className="tip-wrap">
<button
type="button"
className="trigger"
onMouseEnter={() => setShow(true)}
onFocus={() => setShow(true)}
onMouseLeave={() => setShow(false)}
onBlur={() => setShow(false)}
aria-describedby={show ? 'library-tip' : undefined}
>
Hover me
</button>
{show && (
<span id="library-tip" className="tooltip" role="tooltip">
Saved to your library
</span>
)}
</span>
</div>
</main>
);
}
The event pairs support both input modes. Conditional rendering removes the bubble when closed, and the conditional attribute never points at a missing element.
Tab to the button. onFocus calls setShow(true), React renders #library-tip, and the button gains the matching aria-describedby. Tab away and onBlur clears the state, removing both outputs.
aria-describedby when the conditionally rendered element is absent.Escape without moving focus from the trigger.import { useReducer } from 'react';
import './styles.css';
type VisibilityAction = { type: 'show' } | { type: 'hide' };
function visibilityReducer(_: boolean, action: VisibilityAction) {
return action.type === 'show';
}
export default function App() {
const [show, dispatch] = useReducer(visibilityReducer, false);
return (
<main className="container">
<h1>Tooltip</h1>
<div className="stage">
<span className="tip-wrap">
<button
type="button"
className="trigger"
onMouseEnter={() => dispatch({ type: 'show' })}
onFocus={() => dispatch({ type: 'show' })}
onMouseLeave={() => dispatch({ type: 'hide' })}
onBlur={() => dispatch({ type: 'hide' })}
aria-describedby={show ? 'library-tip' : undefined}
>
Hover me
</button>
{show && (
<span id="library-tip" className="tooltip" role="tooltip">
Saved to your library
</span>
)}
</span>
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A tooltip is a short hint that appears while its trigger is hovered or focused. Build the React version with one boolean that controls the bubble and its accessible relationship to the button.
Implement the default App component in App.tsx; it receives no props and renders the complete trigger and tooltip.
Hover me displays Saved to your library; moving the pointer away hides it.aria-describedby="library-tip" and the bubble has id="library-tip" with role="tooltip".useState(false).onMouseEnter/onMouseLeave with onFocus/onBlur.show is true.aria-describedby only while its matching tooltip exists.styles.css.Keep visibility in one React state value and derive both the bubble and its accessible description link from it.
A tooltip must respond to pointer and keyboard attention. Showing a bubble is only half the job: while it exists, the trigger should identify it as a description.
show is the single source of truth. Pointer and focus events update it, then React uses the same boolean to render #library-tip and set aria-describedby.
const [show, setShow] = useState(false);
<button onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
Hover me
</button>
This works with a mouse, but keyboard focus never changes show. It also omits the relationship that lets assistive technology treat the bubble as the button's description.
import { useState } from 'react';
import './styles.css';
export default function App() {
const [show, setShow] = useState(false);
return (
<main className="container">
<h1>Tooltip</h1>
<div className="stage">
<span className="tip-wrap">
<button
type="button"
className="trigger"
onMouseEnter={() => setShow(true)}
onFocus={() => setShow(true)}
onMouseLeave={() => setShow(false)}
onBlur={() => setShow(false)}
aria-describedby={show ? 'library-tip' : undefined}
>
Hover me
</button>
{show && (
<span id="library-tip" className="tooltip" role="tooltip">
Saved to your library
</span>
)}
</span>
</div>
</main>
);
}
The event pairs support both input modes. Conditional rendering removes the bubble when closed, and the conditional attribute never points at a missing element.
Tab to the button. onFocus calls setShow(true), React renders #library-tip, and the button gains the matching aria-describedby. Tab away and onBlur clears the state, removing both outputs.
aria-describedby when the conditionally rendered element is absent.Escape without moving focus from the trigger.import { useReducer } from 'react';
import './styles.css';
type VisibilityAction = { type: 'show' } | { type: 'hide' };
function visibilityReducer(_: boolean, action: VisibilityAction) {
return action.type === 'show';
}
export default function App() {
const [show, dispatch] = useReducer(visibilityReducer, false);
return (
<main className="container">
<h1>Tooltip</h1>
<div className="stage">
<span className="tip-wrap">
<button
type="button"
className="trigger"
onMouseEnter={() => dispatch({ type: 'show' })}
onFocus={() => dispatch({ type: 'show' })}
onMouseLeave={() => dispatch({ type: 'hide' })}
onBlur={() => dispatch({ type: 'hide' })}
aria-describedby={show ? 'library-tip' : undefined}
>
Hover me
</button>
{show && (
<span id="library-tip" className="tooltip" role="tooltip">
Saved to your library
</span>
)}
</span>
</div>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.