|$ curl https://forge-ai.dev/api/markdown?path=docs/components/toolbar
$cat docs/toolbar.md
updated Recently·25 min read·published
Toolbar
Introduction
A toolbar is a container for a group of controls (buttons, toggles, dropdowns) that perform related actions. Toolbars are common in rich text editors, media players, and IDEs. Proper ARIA semantics ensure screen readers announce the toolbar and its controls correctly.
Basic Toolbar
Toolbar.tsx
TypeScript
| 1 | "use client"; |
| 2 | import { useState, useRef } from "react"; |
| 3 | |
| 4 | function Toolbar() { |
| 5 | const [bold, setBold] = useState(false); |
| 6 | const [italic, setItalic] = useState(false); |
| 7 | const [underline, setUnderline] = useState(false); |
| 8 | const toolbarRef = useRef<HTMLDivElement>(null); |
| 9 | |
| 10 | // Roving tabindex: only one button is tabbable at a time |
| 11 | const handleKeyDown = (e: React.KeyboardEvent) => { |
| 12 | const buttons = Array.from(toolbarRef.current!.querySelectorAll("button")); |
| 13 | const currentIndex = buttons.indexOf(e.target as HTMLButtonElement); |
| 14 | |
| 15 | let nextIndex = currentIndex; |
| 16 | if (e.key === "ArrowRight") nextIndex = (currentIndex + 1) % buttons.length; |
| 17 | if (e.key === "ArrowLeft") nextIndex = (currentIndex - 1 + buttons.length) % buttons.length; |
| 18 | if (e.key === "Home") nextIndex = 0; |
| 19 | if (e.key === "End") nextIndex = buttons.length - 1; |
| 20 | |
| 21 | if (nextIndex !== currentIndex) { |
| 22 | e.preventDefault(); |
| 23 | buttons[nextIndex].focus(); |
| 24 | } |
| 25 | }; |
| 26 | |
| 27 | return ( |
| 28 | <div |
| 29 | ref={toolbarRef} |
| 30 | role="toolbar" |
| 31 | aria-label="Text formatting" |
| 32 | aria-orientation="horizontal" |
| 33 | className="flex items-center gap-1 p-2 bg-[#1a1a28] border border-[#333] rounded-lg" |
| 34 | onKeyDown={handleKeyDown} |
| 35 | > |
| 36 | <button |
| 37 | role="button" |
| 38 | aria-pressed={bold} |
| 39 | tabIndex={bold ? 0 : -1} |
| 40 | className={`px-2 py-1 rounded text-sm ${bold ? "bg-[#00FF41] text-black" : "bg-white/10 text-[#E0E0E0]"}`} |
| 41 | onClick={() => setBold(!bold)} |
| 42 | > |
| 43 | B |
| 44 | </button> |
| 45 | <button |
| 46 | aria-pressed={italic} |
| 47 | tabIndex={italic ? 0 : -1} |
| 48 | className={`px-2 py-1 rounded text-sm ${italic ? "bg-[#00FF41] text-black" : "bg-white/10 text-[#E0E0E0]"}`} |
| 49 | onClick={() => setItalic(!italic)} |
| 50 | > |
| 51 | I |
| 52 | </button> |
| 53 | <button |
| 54 | aria-pressed={underline} |
| 55 | tabIndex={underline ? 0 : -1} |
| 56 | className={`px-2 py-1 rounded text-sm ${underline ? "bg-[#00FF41] text-black" : "bg-white/10 text-[#E0E0E0]"}`} |
| 57 | onClick={() => setUnderline(!underline)} |
| 58 | > |
| 59 | U |
| 60 | </button> |
| 61 | <div role="separator" className="w-px h-5 bg-[#333] mx-1" /> |
| 62 | <button tabIndex={-1} className="px-2 py-1 rounded text-sm bg-white/10 text-[#E0E0E0]">Link</button> |
| 63 | </div> |
| 64 | ); |
| 65 | } |
ℹ
info
Use aria-pressed for toggle buttons and role="toolbar" for the container. Arrow keys should move focus between buttons (roving tabindex pattern).
Overflow Menu
ToolbarOverflow.tsx
TypeScript
| 1 | // Toolbar overflow: move hidden items to a "..." menu |
| 2 | "use client"; |
| 3 | import { useState, useRef, useEffect } from "react"; |
| 4 | |
| 5 | function ToolbarWithOverflow() { |
| 6 | const [showOverflow, setShowOverflow] = useState(false); |
| 7 | const overflowRef = useRef<HTMLDivElement>(null); |
| 8 | |
| 9 | useEffect(() => { |
| 10 | if (!showOverflow) return; |
| 11 | const handleClick = (e: MouseEvent) => { |
| 12 | if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) { |
| 13 | setShowOverflow(false); |
| 14 | } |
| 15 | }; |
| 16 | document.addEventListener("mousedown", handleClick); |
| 17 | return () => document.removeEventListener("mousedown", handleClick); |
| 18 | }, [showOverflow]); |
| 19 | |
| 20 | return ( |
| 21 | <div role="toolbar" aria-label="Formatting" className="flex items-center gap-1 p-2"> |
| 22 | <button className="px-2 py-1 rounded bg-white/10 text-[#E0E0E0]">Bold</button> |
| 23 | <button className="px-2 py-1 rounded bg-white/10 text-[#E0E0E0]">Italic</button> |
| 24 | <div className="relative" ref={overflowRef}> |
| 25 | <button |
| 26 | aria-haspopup="true" |
| 27 | aria-expanded={showOverflow} |
| 28 | onClick={() => setShowOverflow(!showOverflow)} |
| 29 | className="px-2 py-1 rounded bg-white/10 text-[#E0E0E0]" |
| 30 | > |
| 31 | ... |
| 32 | </button> |
| 33 | {showOverflow && ( |
| 34 | <div className="absolute top-full mt-1 right-0 bg-[#1a1a28] border border-[#333] rounded-lg shadow-xl py-1 z-50"> |
| 35 | <button className="block w-full px-3 py-2 text-left text-sm text-[#E0E0E0] hover:bg-white/10">Underline</button> |
| 36 | <button className="block w-full px-3 py-2 text-left text-sm text-[#E0E0E0] hover:bg-white/10">Strikethrough</button> |
| 37 | <button className="block w-full px-3 py-2 text-left text-sm text-[#E0E0E0] hover:bg-white/10">Code</button> |
| 38 | </div> |
| 39 | )} |
| 40 | </div> |
| 41 | </div> |
| 42 | ); |
| 43 | } |
$Blueprint — Engineering Documentation·Section ID: UI-TB-01·Revision: 1.0