Toolbar
Floating per-node action bar inside a Canvas. Anchored below the selected node, surfaces contextual actions like edit, duplicate and delete.
Overview
Toolbar is a thin Gremorie skin over NodeToolbar from @xyflow/react. It renders a small floating row of actions that appears next to a node when the node is selected (or always, depending on isVisible).
Place it inside a <Node> body. xyflow positions it relative to the node automatically (defaulting to Position.Bottom) and re-flows it as the node moves or the canvas pans. The chrome is intentionally minimal: a rounded border, light background and gap-1 flex row that holds whatever buttons you want.
Use it for the most common workflow affordances: edit, duplicate, delete, configure. For more involved controls that should always be visible (like canvas-level zoom and fit), reach for Controls instead.
Preview
'use client';import '@xyflow/react/dist/style.css';import { Toolbar } from '@gremorie/rx-ai';import { Button } from '@gremorie/rx-forms';import { type NodeProps, type NodeTypes, Position, ReactFlow,} from '@xyflow/react';import { Bold, Italic, Underline } from 'lucide-react';import type { ReactNode } from 'react';const ToolbarNode = ({ id, data }: NodeProps) => ( <> <Toolbar isVisible nodeId={id} position={Position.Bottom}> {(data as { toolbar: ReactNode }).toolbar} </Toolbar> <div className="rounded-md border bg-background px-6 py-3 text-sm"> Node </div> </>);const toolbarNodeTypes: NodeTypes = { toolbarNode: ToolbarNode };export function ToolbarPreview() { return ( <div className="h-[220px] w-full"> <ReactFlow edges={[]} fitView nodeTypes={toolbarNodeTypes} nodes={[ { id: '1', type: 'toolbarNode', position: { x: 110, y: 70 }, data: { toolbar: ( <div aria-label="Formatting" className="flex items-center gap-1" role="group" > <Button aria-label="Bold" size="icon-sm" variant="ghost"> <Bold aria-hidden="true" className="size-4" /> </Button> <Button aria-label="Italic" size="icon-sm" variant="ghost"> <Italic aria-hidden="true" className="size-4" /> </Button> <Button aria-label="Underline" size="icon-sm" variant="ghost"> <Underline aria-hidden="true" className="size-4" /> </Button> </div> ), }, }, ]} nodesConnectable={false} nodesDraggable={false} panOnDrag={false} proOptions={{ hideAttribution: true }} zoomOnScroll={false} /> </div> );}Anatomy
Toolbar node action toolbar — leaf primitive (rendered on a Canvas node)Installation
bash npx gremorie@latest add rx-toolbar bash pnpm dlx gremorie@latest add rx-toolbar bash yarn dlx gremorie@latest add rx-toolbar bash bunx --bun gremorie@latest add rx-toolbar Requires @xyflow/react as a peer dependency. Already a transitive dep of the workflow family, so installing rx-canvas, rx-node or any other workflow primitive brings it along.
Import the xyflow stylesheet once at app entry:
import '@xyflow/react/dist/style.css';Usage
import { Node, NodeContent, Toolbar } from "@gremorie/rx-ai";
import { Button } from "@gremorie/rx-forms";
import { PencilIcon, TrashIcon } from "lucide-react";
export function StartNode({ data, ...props }) {
return (
<Node {...props} handles={{ target: false, source: true }}>
<Toolbar>
<Button size="icon" variant="ghost" aria-label="Edit">
<PencilIcon className="size-4" />
</Button>
<Button size="icon" variant="ghost" aria-label="Delete">
<TrashIcon className="size-4" />
</Button>
</Toolbar>
<NodeContent>{data.label}</NodeContent>
</Node>
);
}import { Component } from "@angular/core";
import {
Toolbar,
ToolbarGroup,
ToolbarButton,
} from "@gremorie/ng-ai";
@Component({
selector: "app-example",
standalone: true,
imports: [Toolbar, ToolbarGroup, ToolbarButton],
template: ` <toolbar>
<toolbar-group ariaLabel="Formatting">
<toolbar-button [pressed]="bold" ariaLabel="Bold" (clicked)="bold = !bold">
<span class="font-bold">B</span>
</toolbar-button>
<toolbar-button ariaLabel="Italic">
<span class="italic">I</span>
</toolbar-button>
</toolbar-group>
</toolbar>
`,
})
export class ExampleComponent {
bold = false;
}
The Angular
<toolbar>is the generic primitive (a<div role="toolbar">). Pair it with@xyflow/angularat the app level if you need a canvas-anchored toolbar.
API
<Toolbar>
Wraps NodeToolbar from @xyflow/react. All NodeToolbar props are forwarded.
| Prop | Type | Default | Description |
|---|---|---|---|
position | Position | Position.Bottom | Anchor side. Gremorie overrides the xyflow default to bottom. |
isVisible | boolean | controlled by xyflow | Force visible regardless of selection state. |
offset | number | 10 | Distance (px) from the node edge. |
nodeId | string | string[] | parent node | Bind to a specific node when rendered outside of one. |
align | "start" | "center" | "end" | "center" | Alignment along the chosen edge. |
className | string | - | Extra classes on the toolbar surface. |
Default chrome: flex items-center gap-1 rounded-sm border bg-background p-1.5.
Composition
<Toolbar>lives inside a<Node>. xyflow infers the node ID from the surroundingNodePropscontext, so you typically do not passnodeIdmanually.- Children are usually
Button size="icon"from@gremorie/rx-forms. Each button must have its ownaria-labelbecause they are icon-only. - Visibility follows node selection by default. Click the node and the toolbar pops in; click anywhere else and it disappears.
- For canvas-wide controls (zoom in, zoom out, fit view), use
Controlswhich docks to a corner of the canvas instead.
Variations
Edit and delete only
The default minimal toolbar. Visible only when the node is selected.
<Toolbar>
<Button size="icon" variant="ghost" aria-label="Edit">
<PencilIcon className="size-4" />
</Button>
<Button size="icon" variant="ghost" aria-label="Delete">
<TrashIcon className="size-4" />
</Button>
</Toolbar>Always visible
Pass isVisible to keep the toolbar mounted regardless of selection. Useful for "drag handle" affordances on read-only flows.
<Toolbar isVisible>
<Button size="icon" variant="ghost" aria-label="Drag">
<GripVerticalIcon className="size-4" />
</Button>
</Toolbar>Anchored to the top
Override position when the canvas region below the node is too crowded.
import { Position } from '@xyflow/react';
<Toolbar position={Position.Top}>
<Button size="icon" variant="ghost" aria-label="Configure">
<SettingsIcon className="size-4" />
</Button>
</Toolbar>;Accessibility
- Keyboard: Buttons are real
Buttons, so Tab order follows the DOM and Enter / Space activate them. Selecting a node with the keyboard (after the canvas is focused) also exposes its toolbar. - Icon-only buttons require
aria-label: There is no built-in tooltip onToolbaractions. Either supplyaria-labeldirectly or wrap each button in aTooltip+TooltipTriggerfrom@gremorie/rx-overlays. - Reduced motion: The xyflow positioning animation is purely transform-based. Honor user prefs with
@media (prefers-reduced-motion: reduce)if you add custom transitions. - Screen readers: Because the toolbar appears on selection, announce node selection via
aria-live(or rely on xyflow's selection events) so assistive tech knows new actions are available.