Skip to content

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.

tsx
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 ​

typeExtra propsDescription
'action'onClickOne-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:

PropTypeDescription
idstringUnique key for this item.
labelstringTooltip / accessible label.
iconReactNodeIcon 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?booleanDisables 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.

tsx
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 ​

PropTypeRequiredDescription
type'group'✓Item type discriminant.
idstring✓Button ID and radio group key in ToolbarContext.
labelstring✓Tooltip shown when no sub-item is active.
defaultIconReactNode✓Icon shown when no sub-item is active.
itemsToolbarGroupEntry[]✓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).

tsx
// Uncontrolled — read via useToolbar()
const { getActiveInGroup } = useToolbar();
const activeBrush = getActiveInGroup('brush-tool'); // 'brush-pencil' | 'brush-eraser' | ... | null

Controlled: provide activeItemId and wire onActiveItemChange to your state. RddToolbar never modifies context — you are the single source of truth.

tsx
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 ​

PropTypeDefaultDescription
itemsToolbarItem[]—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.
visiblebooleantrueCollapse 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.
classNamestring—Additional CSS class applied to the root div.
styleReact.CSSProperties—Inline style merged after the collapse style.

visible + ToolbarHandle ​

visible is a controlled prop — the consumer owns the boolean via useState:

tsx
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:

tsx
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();
MethodDescription
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:

tsx
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
}
ValueTypeDescription
getActiveInGroup(group)(group: string) => string | nullReturns the active radio item id in a group, or null if none selected.
setActiveInGroup(group, id)(group: string, id: string | null) => voidProgrammatically activate a radio item (or deselect with null).
isModifierActive(id)(id: string) => booleanReturns true if a toggle modifier is active.
setModifierActive(id, active)(id: string, active: boolean) => voidExplicitly set a toggle modifier's state.
toggleModifier(id)(id: string) => voidFlip 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:

tsx
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 ​

tsx
// 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 top

The 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:

VariableLightDarkDescription
--rdd-toolbar-btn-hover-bgrgba(0,0,0,.05)rgba(255,255,255,.06)Button hover background.
--rdd-toolbar-btn-radio-active-bgrgba(0,102,204,.1)rgba(56,189,248,.14)Radio active button background tint.
--rdd-toolbar-btn-toggle-active-bgaccent 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-activeIcon of a toggle that is on.
--rdd-toolbar-btn-toggle-active-border--rdd-tab-icon-active--rdd-tab-icon-active1px edge of a toggle that is on.
--rdd-chrome-icon-size22px22pxIcon 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-colorrgba(0,0,0,.1)rgba(255,255,255,.09)Separator line color.
--rdd-tab-icon-active#0066cc#38bdf8Accent color for active radio button icon and border. Shared with the sidebar strip.
--rdd-toolbar-btn-active-shadownonenonebox-shadow on active radio/group buttons. Obsidian/Tokyo override with an inset ambient glow.
--rdd-toolbar-btn-active-glownonenonefilter on active radio/group buttons. Obsidian/Tokyo add drop-shadow() for icon glow.
--rdd-toolbar-accent-bar-width3px3pxWidth 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 ​

Released under the MIT License.