Rich Text Toolbar
Machine-coding brief for a rich-text toolbar — contentEditable or execCommand baseline, active states, and a11y.
- machine-coding
- interview
- react
- editor
Problem statement
Build a rich-text toolbar that applies bold/italic/underline (and optionally lists/links) to a contenteditable surface. Interviewers know document.execCommand is deprecated — still OK for interviews if you name the tradeoff and keep the command pattern clean. Production answer points to a proper editor model (ProseMirror, Lexical, etc.).
Requirements
Must have
- Editable region (
contentEditableorrole="textbox") - Toolbar: Bold, Italic, Underline
- Commands apply to current selection
- Active state reflects selection (
queryCommandState) - Keyboard shortcuts: Ctrl/Cmd+B/I/U
- Toolbar buttons are real
<button type="button">with pressed state
Should have
- Unordered list / link create
onChangeHTML or plain text export- Disabled when editor unfocused optional
Nice to have
- Custom undo stack
- Markdown shortcuts
- Sanitized paste
Planning (5 minutes out loud)
- Command pattern —
exec(command, value?) - Selection restore — keep editor focused when clicking toolbar (
onMouseDown preventDefault) - Active states on
selectionchange - MVP — B/I/U + contentEditable; then lists
- Call out execCommand deprecation + XSS if dumping HTML
Architecture
RichTextEditor
├── Toolbar
│ └── ToolbarButton (aria-pressed)
└── EditableSurface
API
type RichTextEditorProps = {
initialHTML?: string;
onChange?: (html: string) => void;
placeholder?: string;
};
Implementation sketch
function ToolbarButton({
label,
command,
active,
onAction,
}: {
label: string;
command: string;
active: boolean;
onAction: (command: string) => void;
}) {
return (
<button
type="button"
aria-label={label}
aria-pressed={active}
onMouseDown={(e) => e.preventDefault()} // keep selection
onClick={() => onAction(command)}
>
{label}
</button>
);
}
function RichTextEditor({ initialHTML = "", onChange }: RichTextEditorProps) {
const ref = useRef<HTMLDivElement>(null);
const [active, setActive] = useState({ bold: false, italic: false, underline: false });
function refreshActive() {
setActive({
bold: document.queryCommandState("bold"),
italic: document.queryCommandState("italic"),
underline: document.queryCommandState("underline"),
});
}
useEffect(() => {
document.addEventListener("selectionchange", refreshActive);
return () => document.removeEventListener("selectionchange", refreshActive);
}, []);
function exec(command: string, value?: string) {
ref.current?.focus();
document.execCommand(command, false, value);
onChange?.(ref.current?.innerHTML ?? "");
refreshActive();
}
return (
<div className="rte">
<div role="toolbar" aria-label="Text formatting">
<ToolbarButton label="Bold" command="bold" active={active.bold} onAction={exec} />
<ToolbarButton label="Italic" command="italic" active={active.italic} onAction={exec} />
<ToolbarButton
label="Underline"
command="underline"
active={active.underline}
onAction={exec}
/>
<ToolbarButton
label="Bulleted list"
command="insertUnorderedList"
active={false}
onAction={exec}
/>
</div>
<div
ref={ref}
className="surface"
contentEditable
role="textbox"
aria-multiline="true"
aria-label="Rich text editor"
suppressContentEditableWarning
dangerouslySetInnerHTML={{ __html: initialHTML }}
onInput={() => onChange?.(ref.current?.innerHTML ?? "")}
onKeyDown={(e) => {
const mod = e.metaKey || e.ctrlKey;
if (mod && e.key.toLowerCase() === "b") {
e.preventDefault();
exec("bold");
}
if (mod && e.key.toLowerCase() === "i") {
e.preventDefault();
exec("italic");
}
if (mod && e.key.toLowerCase() === "u") {
e.preventDefault();
exec("underline");
}
}}
/>
</div>
);
}
Link command
function createLink() {
const url = window.prompt("URL");
if (!url) return;
document.execCommand("createLink", false, url);
}
Sanitize URLs (https: only) in anything beyond a toy.
Accessibility essentials
- Toolbar:
role="toolbar"; buttonsaria-pressedfor toggles - Editor:
role="textbox"+aria-multiline="true"+ label onMouseDown preventDefaulton toolbar so focus/selection isn’t lost- Announce that shortcuts exist (visually hidden help)
Performance notes
selectionchangefires often — fine for this scope- Large documents: contentEditable becomes painful → real editor framework
- Avoid serializing HTML on every animation frame
Footguns
- Click toolbar collapses selection without mousedown preventDefault
- XSS via
innerHTML/ paste - Controlled React value fighting contentEditable every render
- Using execCommand in production without a path off it
- Placeholder hacks breaking empty detection
Interview out-loud answer
I’d ship a contentEditable surface with a command toolbar, preserving selection via mousedown preventDefault, and reflecting active marks with queryCommandState. Shortcuts mirror buttons. I’d explicitly say execCommand is fine for interview MVP but production needs a document model (Lexical/ProseMirror) and sanitization. Paste and links are the first security footguns.
Related on this site
- Split Markdown Editor
- Code Editor Line Numbers
- Typeahead Mentions
- Machine Coding Interview Framework