Toolbar
In reviewLive · web
Formatting: toggles stay pressed · Tab in, Arrow keys move, the ⋯ overflows
Contextual: a selection action bar (count · actions · close)
3 selected
Primary actions: labelled buttons + icon buttons, one filled primary
Responsive: when space is tight, extra items fold into the overflow
Vertical: Up / Down move focus
Wrapping: a disabled control stays legible (skipped by arrows)
Marks: bold · align left · last action none. Pointer + keyboard both work; the island is live.
// Web: React, consuming @dizyx/nockerl-tokens. There is NO bespoke Toolbar*NockerlButton// family: a toolbar SLOTS the shipped primitives (NockerlIconButton / NockerlButton / NockerlDivider /// NockerlSegmentedControl) inside a role="toolbar" container that owns the roving tabindex.// Toggles use the primitives' `pressed` prop (soft-cyan wash + aria-pressed).import { NockerlIconButton, NockerlButton, NockerlDivider, NockerlSegmentedControl } from '@dizyx/nockerl-react';
export function FormattingToolbar() { const [marks, setMarks] = useState({ bold: true, italic: false }); const [view, setView] = useState('split'); // reg / tab / onKey come from the toolbar container's roving-tabindex hook: each // child registers its DOM node (NockerlIconButton/NockerlButton forward the ref), is the sole tab // stop when tab === idx, and forwards Arrow/Home/End to the container. See ToolbarDemo. return ( <Toolbar label="Text formatting"> {({ reg, tab, onKey }) => ( <> <span className="toolbar-group"> <NockerlIconButton icon={<BoldIcon />} label="Bold" pressed={marks.bold} ref={(el) => reg(0, el)} tabIndex={tab === 0 ? 0 : -1} onKeyDown={(e) => onKey(e, 0)} onClick={() => setMarks((m) => ({ ...m, bold: !m.bold }))} /> <NockerlIconButton icon={<ItalicIcon />} label="Italic" pressed={marks.italic} ref={(el) => reg(1, el)} tabIndex={tab === 1 ? 0 : -1} onKeyDown={(e) => onKey(e, 1)} onClick={() => setMarks((m) => ({ ...m, italic: !m.italic }))} /> </span> <NockerlDivider orientation="vertical" /> {/* a mutually-exclusive group → the real NockerlSegmentedControl (single-select). NockerlIcon keys come from SEGMENT_ICONS: split · list · unified · grid · board · sun · moon · auto. */} <NockerlSegmentedControl label="View" value={view} onChange={setView} size="sm" segments={[{ value: 'split', icon: 'split' }, { value: 'list', icon: 'list' }, { value: 'grid', icon: 'grid' }]} /> <NockerlDivider orientation="vertical" /> {/* one filled primary (labelled NockerlButton); the ⋯ overflow trigger is a plain NockerlIconButton */} <NockerlButton variant="primary" size="sm" leadingIcon={<PlusIcon />} text="New session" ref={(el) => reg(2, el)} tabIndex={tab === 2 ? 0 : -1} onKeyDown={(e) => onKey(e, 2)} onClick={createSession} /> <NockerlIconButton variant="plain" icon={<KebabIcon />} label="More actions" ref={(el) => reg(3, el)} tabIndex={tab === 3 ? 0 : -1} onKeyDown={(e) => onKey(e, 3)} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(true)} /> </> )} </Toolbar> );}// Android (Jetpack Compose). No single NockerlToolbar component ships yet; the// canonical action strip is a Row of grouped controls on the chrome surface,// composed from the shipped com.nockerl.app.core.ui primitives (NockerlIconButton// PLAIN + a vertical divider + a DropdownMenu overflow). Mirrors ChatInputBar.import com.nockerl.app.core.ui.NockerlIconButtonimport com.nockerl.app.core.theme.LocalNockerlColors
@Composablefun FormattingToolbar(state: EditorState) { val chrome = LocalNockerlColors.current Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier .background(chrome.chromeSurface, NockerlControlShape) .padding(4.dp), ) { // A pressed toggle reuses the segmented active look: soft-cyan wash + cyan glyph. NockerlIconButton(Icons.Default.FormatBold, "Bold", onClick = state::toggleBold, tint = if (state.bold) chrome.accentPrimary else chrome.onChrome) NockerlIconButton(Icons.Default.FormatItalic, "Italic", onClick = state::toggleItalic, tint = if (state.italic) chrome.accentPrimary else chrome.onChrome)
VerticalDivider(Modifier.height(20.dp), color = chrome.divider) // a full-height separator
NockerlIconButton(Icons.Default.FormatListBulleted, "List", onClick = state::toggleList) var overflow by remember { mutableStateOf(false) } Box { NockerlIconButton(Icons.Default.MoreVert, "More actions", onClick = { overflow = true }) DropdownMenu(expanded = overflow, onDismissRequest = { overflow = false }) { DropdownMenuItem(text = { Text("Insert image") }, onClick = state::insertImage) } } }}// macOS SwiftUI. The idiomatic API is the native .toolbar modifier with// ToolbarItemGroup; a Menu in the trailing group is the overflow. (Drift: the// shipped Voice app is a menu-bar utility and uses a custom HStack command bar,// RecordingHUD, instead of .toolbar, so this is the intended idiom, not shipped.).toolbar { ToolbarItemGroup(placement: .principal) { Toggle(isOn: $bold) { Label("Bold", systemImage: "bold") } Toggle(isOn: $italic) { Label("Italic", systemImage: "italic") }
Divider() // a full-height separator between groups
Picker("Alignment", selection: $align) { Image(systemName: "text.alignleft").tag(Align.left) Image(systemName: "text.aligncenter").tag(Align.center) Image(systemName: "text.alignright").tag(Align.right) } .pickerStyle(.segmented) } ToolbarItemGroup(placement: .primaryAction) { Button("New", systemImage: "plus") { createSession() } .buttonStyle(.nockerlPrimary) Menu { // the overflow ⋯ Button("Insert image", systemImage: "photo") { insertImage() } } label: { Label("More actions", systemImage: "ellipsis") } }}Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
label * | string | Accessible name for the strip (wired to role="toolbar" via aria-label). Required: a toolbar is one focus group and needs a name. | |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Layout axis. Arrow keys follow it: Left/Right when horizontal, Up/Down when vertical. |
children | (roving) => ReactNode | The container SLOTS the real primitives: IconButton, Button, Divider, SegmentedControl, and the overflow ⋯ (a plain IconButton + a Menu). NOT a bespoke Toolbar*Button family. The render-prop hands each child its roving plumbing (reg / tab / onKey). | |
IconButton.pressed / Button.pressed | boolean | TOGGLE mode on the real primitive: renders the soft-cyan selection wash and sets aria-pressed. Use for sticky formatting marks (bold/italic) or a labelled toggle (Wrap). Omit for a plain command control. | |
Button.variant | 'primary' | 'ghost' | … | 'primary' | Labelled toolbar buttons are the Button primitive. Use primary for the one filled CTA (max one per strip), ghost for plain labelled actions (Filter / Share). A destructive icon tints via style={{ color: 'var(--color-status-error)' }} (IconButton has no danger idiom). |
roving = { reg, tab, onKey } | { reg; tab; onKey } | Wire each slotted primitive with ref={(el) => reg(idx, el)}, tabIndex={tab === idx ? 0 : -1}, onKeyDown={(e) => onKey(idx)}. Button + IconButton forward the ref and spread these native attrs, so the container keeps exactly one child tabbable and Arrow/Home/End move focus across controls. | |
SegmentedControl | primitive | A mutually-exclusive segment group inside the toolbar (e.g. alignment) is the real SegmentedControl (segments / value / onChange, single-select). Its own role="radiogroup" + roving tabindex nests inside the strip. | |
Overflow ⋯ items | OverflowItem[] | The ⋯ trigger is a plain IconButton opening a Menu of folded-away actions (icon + label, optional checked / danger). Use to collapse low-priority or space-tight controls. |
| Parameter | Type | Default | Description |
|---|---|---|---|
modifier | Modifier | Modifier | Applied to the action Row. Set the chromeSurface background + NockerlControlShape + padding here. |
horizontalArrangement | Arrangement.Horizontal | spacedBy(2.dp) | Gap between controls within a group. Groups are split by a VerticalDivider. |
verticalAlignment | Alignment.Vertical | CenterVertically | Controls are vertically centered and equal-height on the strip. |
NockerlIconButton.icon * | ImageVector | The action glyph. The toolbar action unit (PLAIN idiom, a 12px-control tappable glyph). | |
NockerlIconButton.contentDescription * | String? | Accessibility label; icon-only actions must name themselves. | |
NockerlIconButton.tint | Color | onChrome | Glyph color. A pressed toggle passes accentPrimary (the segmented active look). |
VerticalDivider.color | Color | colors.divider | The full-height separator hairline between groups (Modifier.height(20.dp)). |
DropdownMenu | @Composable | The overflow ⋯, anchored off a trailing MoreVert icon button, holding the folded-away actions. |
SwiftUI builds the toolbar declaratively with the native .toolbar modifier; there is
no single Nockerl Toolbar view. Groups are ToolbarItemGroup, separators are Divider(),
toggles are Toggle, and the overflow is a Menu. State comes from @State/@Binding.
| Style | Type | Default | Description |
|---|---|---|---|
.toolbar { … } | View modifier | Attaches the action strip to a view / window. The system places it as native chrome. | |
ToolbarItemGroup(placement:) | ToolbarContent | A grouped run of controls. .principal centers (formatting); .primaryAction trails (primary + overflow). | |
Toggle(isOn:) | View | A sticky toggle button, the pressed mark. Carries its own isOn binding. | |
Divider() | View | A full-height separator between groups inside a group run. | |
.pickerStyle(.segmented) | View modifier | A connected single-select group (e.g. alignment), the segmented-control idiom inside the toolbar. | |
Menu { … } | View | The overflow ⋯: a trailing menu of folded-away / low-priority actions. |