Toolbar
A row of buttons, toggles and links that acts as a single tab stop with arrow-key navigation — and zero visual opinion.
Formatting toolbar
BitBhToolbar renders role="toolbar" as a single tab stop: Tab moves in and out, ← / →
travel through every control — toggle-group items included — and Home / End jump to the ends. A multiple
BitBhToggleGroup (bold / italic / strikethrough, aria-pressed) and a single one
(alignment, role="radio") share the toolbar's arrow-key navigation with a
BitBhToolbarLink and a BitBhToolbarButton. Name the toolbar with aria-label.
<BitBhToolbar aria-label="Formatting options" class="toolbar">
<BitBhToggleGroup Type="BitBhToggleGroupType.Multiple" @bind-Values="formats"
aria-label="Text style" class="toolbar-toggle-group">
<BitBhToggleGroupItem Value="bold" aria-label="Bold" class="toolbar-toggle"><strong>B</strong></BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="italic" aria-label="Italic" class="toolbar-toggle"><em>I</em></BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="strike" aria-label="Strikethrough" class="toolbar-toggle"><s>S</s></BitBhToggleGroupItem>
</BitBhToggleGroup>
<BitBhToolbarSeparator class="toolbar-separator" />
<BitBhToggleGroup @bind-Value="alignment" AllowEmpty="false"
aria-label="Text alignment" class="toolbar-toggle-group">
<BitBhToggleGroupItem Value="left" aria-label="Align left" class="toolbar-toggle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 3.5h12M2 6.5h8M2 9.5h12M2 12.5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="center" aria-label="Align center" class="toolbar-toggle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 3.5h12M4 6.5h8M2 9.5h12M4 12.5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="right" aria-label="Align right" class="toolbar-toggle">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 3.5h12M6 6.5h8M2 9.5h12M6 12.5h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</BitBhToggleGroupItem>
</BitBhToggleGroup>
<BitBhToolbarLink href="https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/" target="_blank" rel="noopener"
class="toolbar-link">
Toolbar pattern ↗
</BitBhToolbarLink>
<BitBhToolbarButton OnClick='@(() => Log("Shared"))' class="toolbar-button toolbar-button-primary">Share</BitBhToolbarButton>
</BitBhToolbar>
<p class="toolbar-preview" style="@PreviewStyle">
The quick brown fox jumps over the lazy dog.
</p>
<span class="demo-output">
Style: <strong>@(formats.Count == 0 ? "none" : string.Join(", ", formats))</strong> ·
Align: <strong>@alignment</strong> ·
Last action: <strong>@(lastAction ?? "(none)")</strong>
</span>
@code {
private IReadOnlyCollection<string> formats = ["bold"];
private string? alignment = "left";
private string? lastAction;
private void Log(string action) => lastAction = action;
private string PreviewStyle =>
$"text-align: {alignment ?? "left"};"
+ (formats.Contains("bold") ? " font-weight: 600;" : "")
+ (formats.Contains("italic") ? " font-style: italic;" : "")
+ (formats.Contains("strike") ? " text-decoration: line-through;" : "");
}/* All styles are consumer-authored — the library ships zero CSS */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
width: 100%;
max-width: 36rem;
padding: 0.3rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* BitBhToggleGroup renders role="group" (multiple) or role="radiogroup" (single) */
.toolbar-toggle-group { display: flex; gap: 0.125rem; }
.toolbar-toggle,
.toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: 0;
border-radius: 0.4rem;
background: transparent;
font: inherit;
font-size: 0.875rem;
color: #3f3f46;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
}
.toolbar-toggle:hover,
.toolbar-button:hover { background: #f4f4f5; color: #18181b; }
/* The toolbar is one tab stop (roving tabindex): make the focused control obvious */
.toolbar-toggle:focus-visible,
.toolbar-button:focus-visible,
.toolbar-link:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Toggle-group items emit data-state="on|off" (plus aria-pressed / aria-checked) */
.toolbar-toggle[data-state="on"] { background: #eff6ff; color: #1d4ed8; }
/* Disabled toolbar buttons get aria-disabled (not disabled) so they stay focusable */
.toolbar-button[aria-disabled="true"],
.toolbar-button[aria-disabled="true"]:hover { opacity: 0.4; background: transparent; cursor: not-allowed; }
/* A separator's data-orientation is its own: vertical inside a horizontal toolbar */
.toolbar-separator { align-self: stretch; flex-shrink: 0; background: #e4e4e7; }
.toolbar-separator[data-orientation="vertical"] { width: 1px; margin: 0.3rem 0.25rem; }
.toolbar-separator[data-orientation="horizontal"] { height: 1px; margin: 0.25rem 0.3rem; }
.toolbar-link {
margin-left: auto;
padding: 0.2rem 0.45rem;
border-radius: 0.3rem;
font-size: 0.8rem;
color: #71717a;
text-decoration: none;
white-space: nowrap;
}
.toolbar-link:hover { color: #2563eb; text-decoration: underline; }
.toolbar-button-primary,
.toolbar-button-primary:hover {
padding: 0 0.85rem;
background: #2563eb;
color: #fff;
font-weight: 500;
}
.toolbar-button-primary:hover { background: #1d4ed8; }
.toolbar-preview {
width: 100%;
max-width: 36rem;
padding: 0.85rem 1rem;
border: 1px dashed #d4d4d8;
border-radius: 0.6rem;
font-size: 0.95rem;
color: #3f3f46;
}Disabled buttons stay focusable
Following the APG, a Disabled BitBhToolbarButton gets aria-disabled="true"
and data-disabled instead of the disabled attribute: arrow keys still land on it, so
keyboard and screen-reader users can discover it, but clicks are ignored. Paste and Clear stay reachable while
the clipboard is empty — cut or copy to enable them.
<BitBhToolbar aria-label="Clipboard" class="toolbar">
<BitBhToolbarButton OnClick="Cut" class="toolbar-button">✂️ Cut</BitBhToolbarButton>
<BitBhToolbarButton OnClick="Copy" class="toolbar-button">📋 Copy</BitBhToolbarButton>
<BitBhToolbarButton OnClick="Paste" Disabled="@(clipboard is null)" class="toolbar-button">📥 Paste</BitBhToolbarButton>
<BitBhToolbarSeparator class="toolbar-separator" />
<BitBhToolbarButton OnClick="ClearClipboard" Disabled="@(clipboard is null)" class="toolbar-button">Clear</BitBhToolbarButton>
</BitBhToolbar>
<span class="demo-output">
Clipboard: <strong>@(clipboard ?? "(empty)")</strong> · Pasted <strong>@pasteCount</strong>×
</span>
@code {
private string? clipboard;
private int pasteCount;
private void Cut() => clipboard = "“Selected text” (cut)";
private void Copy() => clipboard = "“Selected text”";
private void Paste() => pasteCount++;
private void ClearClipboard() => clipboard = null;
}/* All styles are consumer-authored — the library ships zero CSS */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
width: 100%;
max-width: 36rem;
padding: 0.3rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* BitBhToggleGroup renders role="group" (multiple) or role="radiogroup" (single) */
.toolbar-toggle-group { display: flex; gap: 0.125rem; }
.toolbar-toggle,
.toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: 0;
border-radius: 0.4rem;
background: transparent;
font: inherit;
font-size: 0.875rem;
color: #3f3f46;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
}
.toolbar-toggle:hover,
.toolbar-button:hover { background: #f4f4f5; color: #18181b; }
/* The toolbar is one tab stop (roving tabindex): make the focused control obvious */
.toolbar-toggle:focus-visible,
.toolbar-button:focus-visible,
.toolbar-link:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Toggle-group items emit data-state="on|off" (plus aria-pressed / aria-checked) */
.toolbar-toggle[data-state="on"] { background: #eff6ff; color: #1d4ed8; }
/* Disabled toolbar buttons get aria-disabled (not disabled) so they stay focusable */
.toolbar-button[aria-disabled="true"],
.toolbar-button[aria-disabled="true"]:hover { opacity: 0.4; background: transparent; cursor: not-allowed; }
/* A separator's data-orientation is its own: vertical inside a horizontal toolbar */
.toolbar-separator { align-self: stretch; flex-shrink: 0; background: #e4e4e7; }
.toolbar-separator[data-orientation="vertical"] { width: 1px; margin: 0.3rem 0.25rem; }
.toolbar-separator[data-orientation="horizontal"] { height: 1px; margin: 0.25rem 0.3rem; }Vertical orientation
Orientation="BitBhRovingOrientation.Vertical" switches navigation to ↑ / ↓ and sets
aria-orientation and data-orientation on the toolbar; its separators flip to
horizontal. The tool picker is a single-select toggle group (AllowEmpty="false" keeps one tool
active), and Undo / Redo disable themselves — while staying focusable — when their stack is empty.
<div class="toolbar-canvas">
<BitBhToolbar Orientation="BitBhRovingOrientation.Vertical" aria-label="Drawing tools" class="toolbar">
<BitBhToggleGroup @bind-Value="tool" AllowEmpty="false" aria-label="Tool" class="toolbar-toggle-group">
<BitBhToggleGroupItem Value="Select" aria-label="Select" title="Select" class="toolbar-toggle">↖</BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="Pen" aria-label="Pen" title="Pen" class="toolbar-toggle">✎</BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="Shape" aria-label="Shape" title="Shape" class="toolbar-toggle">▭</BitBhToggleGroupItem>
<BitBhToggleGroupItem Value="Text" aria-label="Text" title="Text" class="toolbar-toggle">T</BitBhToggleGroupItem>
</BitBhToggleGroup>
<BitBhToolbarSeparator class="toolbar-separator" />
<BitBhToolbarButton OnClick="Undo" Disabled="@(undoDepth == 0)" aria-label="Undo" title="Undo"
class="toolbar-button">↶</BitBhToolbarButton>
<BitBhToolbarButton OnClick="Redo" Disabled="@(redoDepth == 0)" aria-label="Redo" title="Redo"
class="toolbar-button">↷</BitBhToolbarButton>
</BitBhToolbar>
<div class="toolbar-stage">
<strong>@tool</strong>
<span>Undo stack: @undoDepth · Redo stack: @redoDepth</span>
</div>
</div>
@code {
private string? tool = "Pen";
private int undoDepth = 3;
private int redoDepth;
private void Undo() { undoDepth--; redoDepth++; }
private void Redo() { redoDepth--; undoDepth++; }
}/* All styles are consumer-authored — the library ships zero CSS */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
width: 100%;
max-width: 36rem;
padding: 0.3rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.6rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
/* BitBhToggleGroup renders role="group" (multiple) or role="radiogroup" (single) */
.toolbar-toggle-group { display: flex; gap: 0.125rem; }
.toolbar-toggle,
.toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
border: 0;
border-radius: 0.4rem;
background: transparent;
font: inherit;
font-size: 0.875rem;
color: #3f3f46;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
}
.toolbar-toggle:hover,
.toolbar-button:hover { background: #f4f4f5; color: #18181b; }
/* The toolbar is one tab stop (roving tabindex): make the focused control obvious */
.toolbar-toggle:focus-visible,
.toolbar-button:focus-visible,
.toolbar-link:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
/* Toggle-group items emit data-state="on|off" (plus aria-pressed / aria-checked) */
.toolbar-toggle[data-state="on"] { background: #eff6ff; color: #1d4ed8; }
/* Disabled toolbar buttons get aria-disabled (not disabled) so they stay focusable */
.toolbar-button[aria-disabled="true"],
.toolbar-button[aria-disabled="true"]:hover { opacity: 0.4; background: transparent; cursor: not-allowed; }
/* A separator's data-orientation is its own: vertical inside a horizontal toolbar */
.toolbar-separator { align-self: stretch; flex-shrink: 0; background: #e4e4e7; }
.toolbar-separator[data-orientation="vertical"] { width: 1px; margin: 0.3rem 0.25rem; }
.toolbar-separator[data-orientation="horizontal"] { height: 1px; margin: 0.25rem 0.3rem; }
/* aria-orientation / data-orientation="vertical" come from Orientation */
.toolbar[data-orientation="vertical"] {
flex-direction: column;
flex-wrap: nowrap;
flex-shrink: 0;
width: auto;
}
.toolbar[data-orientation="vertical"] .toolbar-toggle-group { flex-direction: column; }
.toolbar-canvas {
display: flex;
align-items: flex-start;
gap: 0.75rem;
width: 100%;
max-width: 36rem;
}
.toolbar-stage {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
min-height: 14rem;
border: 1px dashed #d4d4d8;
border-radius: 0.6rem;
background: radial-gradient(#e4e4e7 1px, transparent 1px) 0 0 / 16px 16px, #fafafa;
font-size: 0.8rem;
color: #71717a;
}
.toolbar-stage strong { font-size: 1.1rem; color: #18181b; }