Build a small rich-text editor: a contentEditable region plus a Bold / Italic / Underline / Bullet-list toolbar. Clicking a button formats the current selection, and each button lights up when the caret sits inside text that already has that format. This is the classic document.execCommand editor — the same API that powered compose boxes like Gmail's for years.
The starter App.tsx renders the toolbar and a contentEditable editor with seed text, but the buttons do nothing. Wire it up:
onClick runs document.execCommand(cmd) — 'bold', 'italic', 'underline', or 'insertUnorderedList' — which mutates the editable DOM for the current selection.active object in useState ({ bold, italic, underline, list }, all false). Add the active class to a button when its command is currently on.active from document.queryCommandState(cmd) on the editor's onKeyUp / onMouseUp and on a document selectionchange listener (added in useEffect, cleaned up on unmount).onMouseDown={(e) => e.preventDefault()} to every button so clicking it doesn't blur the editor and drop the selection before execCommand runs.editor and click B: it becomes bold, and the B button turns green. Move the caret out of the bold text and B goes back to inactive.queryCommandState reports the caret's real format.document.execCommand('insertUnorderedList') wraps it in a ul / li.execCommand is deprecated but still ships in every browser; it's the pragmatic way to build a toolbar without a full document model. The solution covers the modern alternative.execCommand writes straight into the contentEditable element — your active state only reads back what's there; it never controls the content.styles.css; focus on the behavior.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.