Toolbar
<RddToolbar> is a vertical (or horizontal) strip that hosts action buttons, mutually-exclusive radio tool groups, and independent toggle modifiers. Its state lives library-wide inside DockableDesktopProvider, so any panel can read or change the active tool via the useToolbar() hook.
Basic usage
Place <RddToolbar> as a sibling of <RddSidebar> (which wraps <RddDesktop>) inside your workspace container. The toolbar and the sidebar sit side-by-side — neither wraps the other.
import {
DockableDesktopProvider,
RddDesktop,
RddSidebar,
RddToolbar,
type ToolbarItem,
} from 'react-dockable-desktop';
import { useState } from 'react';
const toolbarItems: ToolbarItem[] = [
{
type: 'radio', id: 'tool-cursor', group: 'tool', label: 'Select',
icon: <CursorIcon />,
},
{
type: 'radio', id: 'tool-pen', group: 'tool', label: 'Draw',
icon: <PenIcon />,
},
{ type: 'separator' },
{
type: 'toggle', id: 'snap', label: 'Snap to Grid',
icon: <SnapIcon />,
},
{ type: 'separator' },
{
type: 'action', id: 'layers', label: 'Open Layers',
icon: <LayersIcon />,
onClick: () => workspace.openPanel('layertree-main', 'layertree'),
},
];
export default function App() {
return (
<DockableDesktopProvider workspace={workspace}>
<div className="rdd-fill-viewport" style={{ display: 'flex' }}>
<RddToolbar position="left" items={toolbarItems} />
<RddSidebar position="right" tabs={sidebarTabs}>
<RddDesktop />
</RddSidebar>
</div>
</DockableDesktopProvider>
);
}Item types
type | Extra props | Description |
|---|---|---|
'action' | onClick | One-shot action button. Click fires onClick and does not change radio/toggle state. |
'radio' | group, onActivate? | Mutually-exclusive within a named group. Only one item per group can be active at a time. |
'toggle' | onToggle?, active? | Independent on/off modifier. Multiple toggles can be active simultaneously. Omit active for uncontrolled (state lives in ToolbarContext, keyed by id); pass it (even false) for controlled mode — the component reads active instead and skips writing to context on click. |
'group' | defaultIcon, items, activeItemId?, onActiveItemChange? | Collapsed tool-family that opens a flyout listing sub-tools with radio semantics. See Group items below. |
'separator' | — | A thin visual divider between button groups. |
Controlled toggles are what let independent panel instances report independent active state — see Panel Contributions → for the full pattern (e.g. two maps each with their own selected tool).
Every item except 'separator' also accepts:
| Prop | Type | Description |
|---|---|---|
id | string | Unique key for this item. |
label | string | Tooltip / accessible label. |
icon | ReactNode | Icon displayed in the button. Recommended: 16×16 SVG, stroke="currentColor". A 'group' item takes defaultIcon instead — shown until one of its sub-tools is active, whose icon then replaces it. |
disabled? | boolean | Disables the button; renders at 35% opacity. |
Group items
A 'group' item is a single button that expands into a flyout panel listing mutually-exclusive sub-tools. The button's icon morphs to show whichever sub-tool is currently active. Sub-tools have the same radio semantics as 'radio' items — only one can be active at a time.
const toolbarItems: ToolbarItem[] = [
{
type: 'group',
id: 'brush-tool',
label: 'Brush tools',
defaultIcon: <BrushIcon />, // shown when nothing is selected
items: [
{ id: 'brush-pencil', label: 'Pencil', icon: <PencilIcon />, shortcut: 'B',
onActivate: id => console.log('activated', id) },
{ id: 'brush-eraser', label: 'Eraser', icon: <EraserIcon />, shortcut: 'E' },
{ type: 'separator' },
{ id: 'brush-fill', label: 'Fill', icon: <FillIcon /> },
],
},
];ToolbarGroupItem props
| Prop | Type | Required | Description |
|---|---|---|---|
type | 'group' | ✓ | Item type discriminant. |
id | string | ✓ | Button ID and radio group key in ToolbarContext. |
label | string | ✓ | Tooltip shown when no sub-item is active. |
defaultIcon | ReactNode | ✓ | Icon shown when no sub-item is active. |
items | ToolbarGroupEntry[] | ✓ | Sub-items and separators in the flyout. |
disabled? | boolean | — | Disables the entire group button. |
activeItemId? | string | null | — | Controlled mode. When provided (even as null), the component reads this prop instead of ToolbarContext and calls onActiveItemChange on click without updating context. Omit for uncontrolled. |
onActiveItemChange? | (id: string) => void | — | Controlled mode. Called when the user selects a sub-item. You must update activeItemId in response. |
Controlled vs uncontrolled
Uncontrolled (default): omit activeItemId. The active sub-item is stored in ToolbarContext and readable via getActiveInGroup(id).
// Uncontrolled — read via useToolbar()
const { getActiveInGroup } = useToolbar();
const activeBrush = getActiveInGroup('brush-tool'); // 'brush-pencil' | 'brush-eraser' | ... | nullControlled: provide activeItemId and wire onActiveItemChange to your state. RddToolbar never modifies context — you are the single source of truth.
const [activeBrush, setActiveBrush] = useState<string | null>(null);
<RddToolbar items={[{
type: 'group', id: 'brush-tool', label: 'Brush tools',
defaultIcon: <BrushIcon />,
items: brushSubItems,
activeItemId: activeBrush,
onActiveItemChange: setActiveBrush,
}]} />RddToolbarProps reference
| Prop | Type | Default | Description |
|---|---|---|---|
items | ToolbarItem[] | — | Required. Ordered list of items to render. |
position | 'left' | 'right' | 'top' | 'bottom' | 'left' | Side the strip attaches to. Controls orientation (vertical vs horizontal) and border direction. |
visible | boolean | true | Collapse the strip to zero width/height via CSS transition. State is preserved — no unmount. |
onVisibilityChange | (visible: boolean) => void | — | Called when show/hide/toggle is invoked on the imperative handle. Wire to your state setter. |
className | string | — | Additional CSS class applied to the root div. |
style | React.CSSProperties | — | Inline style merged after the collapse style. |
visible + ToolbarHandle
visible is a controlled prop — the consumer owns the boolean via useState:
const [showToolbar, setShowToolbar] = useState(true);
<RddToolbar
position="left"
items={toolbarItems}
visible={showToolbar}
onVisibilityChange={setShowToolbar}
/>
// Anywhere in the UI:
<button onClick={() => setShowToolbar(v => !v)}>Toggle Toolbar</button>For imperative control, use forwardRef and ToolbarHandle:
import { useRef } from 'react';
import type { ToolbarHandle } from 'react-dockable-desktop';
const toolbarRef = useRef<ToolbarHandle>(null);
<RddToolbar ref={toolbarRef} ... visible={showToolbar} onVisibilityChange={setShowToolbar} />
// Imperative methods delegate to onVisibilityChange:
toolbarRef.current?.show();
toolbarRef.current?.hide();
toolbarRef.current?.toggle();| Method | Description |
|---|---|
show() | Calls onVisibilityChange(true). |
hide() | Calls onVisibilityChange(false). |
toggle() | Calls onVisibilityChange(!current). |
useToolbar() hook
Read and write toolbar state from any component inside <DockableDesktopProvider> — including floating panels and docked panels far removed from the <RddToolbar> component itself:
import { useToolbar } from 'react-dockable-desktop';
function MapPanel() {
const { getActiveInGroup, isModifierActive } = useToolbar();
const activeTool = getActiveInGroup('tool'); // 'tool-cursor' | 'tool-pen' | null
const snapActive = isModifierActive('snap');
// React to toolbar state in your map controller
}| Value | Type | Description |
|---|---|---|
getActiveInGroup(group) | (group: string) => string | null | Returns the active radio item id in a group, or null if none selected. |
setActiveInGroup(group, id) | (group: string, id: string | null) => void | Programmatically activate a radio item (or deselect with null). |
isModifierActive(id) | (id: string) => boolean | Returns true if a toggle modifier is active. |
setModifierActive(id, active) | (id: string, active: boolean) => void | Explicitly set a toggle modifier's state. |
toggleModifier(id) | (id: string) => void | Flip a toggle modifier. |
WARNING
useToolbar() throws if called outside <DockableDesktopProvider>. Reusable components that might render in tests or standalone should ensure a provider is present (or wrap the call in a try/catch if they genuinely need to tolerate its absence).
External control pattern
Toolbars often need to reflect state changes that originate outside the UI — for example, a map SDK that fires a controllerchanged event when the user activates a tool programmatically. Use setActiveInGroup to sync back:
import { useToolbar } from 'react-dockable-desktop';
function MapController() {
const { setActiveInGroup } = useToolbar();
useEffect(() => {
const unsubscribe = mapController.on('controllerchanged', ({ id }) => {
// Mirror external tool change to the toolbar
setActiveInGroup('tool', id);
});
return unsubscribe;
}, [setActiveInGroup]);
// ...
}Position variants
// Vertical strips (width: 48px, height: 100%)
<RddToolbar position="left" items={items} /> // border on the right
<RddToolbar position="right" items={items} /> // border on the left
// Horizontal strips (height: 48px, width: 100%)
<RddToolbar position="top" items={items} /> // border on the bottom
<RddToolbar position="bottom" items={items} /> // border on the topThe active accent border on radio items sits on the button's edge on the toolbar's own side: the left edge on a left toolbar, the right edge on a right one, the bottom edge on a top toolbar (facing the workspace) and the top edge on a bottom one. On skins like slate and macos the bar is replaced entirely by a floating chip shape; on nord it becomes a short horizontal line drawn below the icon (above it on a bottom toolbar).
Theming CSS variables
All toolbar colors use CSS custom properties that cascade from [data-color-scheme] and [data-rdd-skin]. You can override them for your own skin without touching component CSS:
| Variable | Light | Dark | Description |
|---|---|---|---|
--rdd-toolbar-btn-hover-bg | rgba(0,0,0,.05) | rgba(255,255,255,.06) | Button hover background. |
--rdd-toolbar-btn-radio-active-bg | rgba(0,102,204,.1) | rgba(56,189,248,.14) | Radio active button background tint. |
--rdd-toolbar-btn-toggle-active-bg | accent at 16% | accent at 22% | Background of a toggle that is on. |
--rdd-toolbar-btn-toggle-active-color | --rdd-tab-icon-active | --rdd-tab-icon-active | Icon of a toggle that is on. |
--rdd-toolbar-btn-toggle-active-border | --rdd-tab-icon-active | --rdd-tab-icon-active | 1px edge of a toggle that is on. |
--rdd-chrome-icon-size | 22px | 22px | Icon size in toolbar buttons (and the sidebar rail). Icon fonts follow it as font-size, SVG icons as width and height; pass your icons no size. |
--rdd-toolbar-separator-color | rgba(0,0,0,.1) | rgba(255,255,255,.09) | Separator line color. |
--rdd-tab-icon-active | #0066cc | #38bdf8 | Accent color for active radio button icon and border. Shared with the sidebar strip. |
--rdd-toolbar-btn-active-shadow | none | none | box-shadow on active radio/group buttons. Obsidian/Tokyo override with an inset ambient glow. |
--rdd-toolbar-btn-active-glow | none | none | filter on active radio/group buttons. Obsidian/Tokyo add drop-shadow() for icon glow. |
--rdd-toolbar-accent-bar-width | 3px | 3px | Width of the edge accent bar on active buttons. Set to 0px to replace the bar with a chip shape. |
The accent variables are automatically overridden per skin to match each one's visual language: Chrome, Nord, Obsidian and Tokyo Night set their own --rdd-tab-icon-active (Slate only in its light variant), and several skins set their own toolbar active-state tokens. See Per-skin active state design language → for details on customising this in your own skin.
See also
- Sidebar → — collapsible tab strip, hooks
- Panel Contributions → — let panels publish toolbar items only while active
- Per-skin active state design language → — all 7 patterns, token reference, custom skin examples
- Event Bus & Communication → — panels communicating via pub/sub
- Quick Start → — provider setup