Keyboard-navigable toolbar using Radix's roving focus pattern. Arrow keys move focus between items.
import * as RovingFocusGroup from "@radix-ui/react-roving-focus"; import { useState } from "react"; const tools = [ { id: "bold", label: "B", title: "Bold" }, { id: "italic", label: "I", title: "Italic" }, { id: "underline", label: "U", title: "Underline" }, { id: "strike", label: "S", title: "Strikethrough" }, { id: "code", label: "<>", title: "Code" }, ]; export function Toolbar() { const [active, setActive] = useState<string[]>([]); const toggle = (id: string) => setActive((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id], ); return ( <RovingFocusGroup.Root orientation="horizontal" loop style={{ display: "inline-flex", gap: 2, padding: 4, borderRadius: 8, border: "1px solid #e5e5e5", background: "#fafafa", }} > {tools.map((tool) => { const isActive = active.includes(tool.id); return ( <RovingFocusGroup.Item key={tool.id} asChild> <button type="button" title={tool.title} onClick={() => toggle(tool.id)} style={{ width: 36, height: 36, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: 6, border: "none", cursor: "pointer", fontSize: 14, fontWeight: isActive ? 700 : 400, fontStyle: tool.id === "italic" ? "italic" : "normal", textDecoration: tool.id === "underline" ? "underline" : tool.id === "strike" ? "line-through" : "none", background: isActive ? "#2563eb" : "transparent", color: isActive ? "#fff" : "#333", transition: "all 150ms ease", outline: "none", }} onFocus={(e) => { e.currentTarget.style.boxShadow = "0 0 0 2px #2563eb40"; }} onBlur={(e) => { e.currentTarget.style.boxShadow = "none"; }} > {tool.label} </button> </RovingFocusGroup.Item> ); })} </RovingFocusGroup.Root> ); }