Skip to main content
Gremorie

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/angular at the app level if you need a canvas-anchored toolbar.

API

<Toolbar>

Wraps NodeToolbar from @xyflow/react. All NodeToolbar props are forwarded.

PropTypeDefaultDescription
positionPositionPosition.BottomAnchor side. Gremorie overrides the xyflow default to bottom.
isVisiblebooleancontrolled by xyflowForce visible regardless of selection state.
offsetnumber10Distance (px) from the node edge.
nodeIdstring | string[]parent nodeBind to a specific node when rendered outside of one.
align"start" | "center" | "end""center"Alignment along the chosen edge.
classNamestring-Extra classes on the toolbar surface.

Default chrome: flex items-center gap-1 rounded-sm border bg-background p-1.5.

Composition

  1. <Toolbar> lives inside a <Node>. xyflow infers the node ID from the surrounding NodeProps context, so you typically do not pass nodeId manually.
  2. Children are usually Button size="icon" from @gremorie/rx-forms. Each button must have its own aria-label because they are icon-only.
  3. Visibility follows node selection by default. Click the node and the toolbar pops in; click anywhere else and it disappears.
  4. For canvas-wide controls (zoom in, zoom out, fit view), use Controls which 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 on Toolbar actions. Either supply aria-label directly or wrap each button in a Tooltip + TooltipTrigger from @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.
  • Canvas - the xyflow surface that hosts nodes and their toolbars
  • Node - the host element for a Toolbar
  • Controls - canvas-level controls docked to a corner
  • Panel - a free-floating panel for larger canvas overlays

On this page