Node
Card-based node primitive for Canvas. Wraps Gremorie Card with target / source xyflow Handles and a header / content / footer composition that matches the rest of the design system.
Overview
Node is the card-shaped vertex you place inside a Canvas. It composes a Card from @gremorie/rx-display with xyflow Handles on the left (target) and right (source), then exposes the same Header / Title / Description / Action / Content / Footer composition as a regular Card.
The wrapper customizes Card for the canvas context: removes default padding so handles align cleanly, narrows the width to w-sm, and skins the header and footer with a secondary surface so they read as distinct from the content body.
Use it as the base for every custom node type. Pair with Toolbar for selection-anchored actions, Edge to connect nodes, and Connection for in-progress drags.
Preview
'use client';import '@xyflow/react/dist/style.css';import { Canvas, Controls, Node, NodeAction, NodeContent, NodeDescription, NodeFooter, NodeHeader, NodeTitle,} from '@gremorie/rx-ai';import { Button } from '@gremorie/rx-forms';import { type Edge as FlowEdge, type Node as FlowNode, type NodeProps, type NodeTypes, ReactFlowProvider,} from '@xyflow/react';import { MoreHorizontalIcon } from 'lucide-react';type AgentNodeData = { title: string; description: string; body: string; source: boolean; target: boolean;};const AgentNode = ({ data }: NodeProps) => { const { title, description, body, source, target } = data as AgentNodeData; return ( <Node handles={{ source, target }}> <NodeHeader> <NodeTitle>{title}</NodeTitle> <NodeDescription>{description}</NodeDescription> <NodeAction> <Button aria-label="Node actions" size="icon" variant="ghost"> <MoreHorizontalIcon className="size-4" /> </Button> </NodeAction> </NodeHeader> <NodeContent> <p className="text-muted-foreground text-sm">{body}</p> </NodeContent> <NodeFooter> <span className="text-muted-foreground text-xs">Ready</span> </NodeFooter> </Node> );};const agentNodeTypes: NodeTypes = { agent: AgentNode };const agentNodes: FlowNode[] = [ { id: '1', type: 'agent', position: { x: 0, y: 0 }, data: { title: 'Plan', description: 'Break the task into steps', body: 'Decompose the request and outline the actions to take.', source: true, target: false, }, }, { id: '2', type: 'agent', position: { x: 360, y: 0 }, data: { title: 'Execute', description: 'Run the planned tools', body: 'Call each tool in order and collect the results.', source: false, target: true, }, },];const agentEdges: FlowEdge[] = [{ id: 'e1-2', source: '1', target: '2' }];export function NodePreview() { return ( <div className="h-[360px] w-full"> <ReactFlowProvider> <Canvas edges={agentEdges} nodeTypes={agentNodeTypes} nodes={agentNodes} > <Controls /> </Canvas> </ReactFlowProvider> </div> );}Anatomy
Node
├─ NodeHeader
│ ├─ NodeTitle
│ ├─ NodeDescription
│ └─ NodeAction top-right slot
├─ NodeContent body
└─ NodeFooterInstallation
bash npx gremorie@latest add rx-node bash pnpm dlx gremorie@latest add rx-node bash yarn dlx gremorie@latest add rx-node bash bunx --bun gremorie@latest add rx-node Brings in rx-display (for Card) as a registry dependency. Requires @xyflow/react as a peer.
Usage
import {
Node,
NodeAction,
NodeContent,
NodeDescription,
NodeFooter,
NodeHeader,
NodeTitle,
} from "@gremorie/rx-ai";
import { Button } from "@gremorie/rx-forms";
import { PlayIcon } from "lucide-react";
const AgentNode = (props) => (
<Node {...props} handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>{props.data.name}</NodeTitle>
<NodeDescription>{props.data.model}</NodeDescription>
<NodeAction>
<Button size="icon" variant="ghost" aria-label="Run">
<PlayIcon className="size-4" />
</Button>
</NodeAction>
</NodeHeader>
<NodeContent>{props.data.prompt}</NodeContent>
<NodeFooter>{props.data.tokenCount} tokens</NodeFooter>
</Node>
);
const nodeTypes = { agent: AgentNode };
The Angular edition of this component is planned; the React edition is production-ready today.
API
<Node>
| Prop | Type | Default | Description |
|---|---|---|---|
handles | { target: boolean; source: boolean } | - | Required. Which sides of the node expose connection handles. target adds a left handle, source adds a right handle. |
className | string | - | Extra classes on the underlying Card. |
Extends all Card props from @gremorie/rx-display. Renders the Card with gap-0 rounded-md p-0 and the node-container class for canvas-level targeting. xyflow forwards selection / dragging props automatically when used inside a registered nodeTypes entry.
<NodeHeader>
Border-bottom header with a secondary background and tight p-3! padding. Hosts the title, description and trailing action.
Extends all CardHeader props.
<NodeTitle> / <NodeDescription> / <NodeAction>
Pass-through wrappers around CardTitle, CardDescription and CardAction. Identical APIs - they exist so the visual identity stays consistent across editions and so consumers can swap implementations without touching every node type.
<NodeContent>
Body slot with p-3 padding. Extends all CardContent props.
<NodeFooter>
Border-top footer with a secondary background and p-3! padding. Mirrors NodeHeader.
Extends all CardFooter props.
Composition
- Register the node type on
Canvasvia thenodeTypesprop. The key (e.g."agent") is what edge data references astype. - Inside the node, declare the
handlesconfig so xyflow knows where connections can land. - Compose with Header / Content / Footer to match the rest of Gremorie. The secondary-toned header and footer create natural visual separation from the body.
- Add a
Toolbarfor selection-anchored actions (edit, delete, configure). For permanent actions inside the card, useNodeActionin the header instead.
Variations
Input-only node (entry point)
Source handle only - no incoming connections.
const StartNode = (props) => (
<Node {...props} handles={{ target: false, source: true }}>
<NodeContent>Start</NodeContent>
</Node>
);Output-only node (terminal)
Target handle only - no outgoing connections.
const EndNode = (props) => (
<Node {...props} handles={{ target: true, source: false }}>
<NodeContent>End</NodeContent>
</Node>
);Full card with header, action and footer
A complete agent node with metadata in the header, prompt in the body, status in the footer and a run action.
<Node {...props} handles={{ target: true, source: true }}>
<NodeHeader>
<NodeTitle>{props.data.name}</NodeTitle>
<NodeDescription>{props.data.model}</NodeDescription>
<NodeAction>
<Button size="icon" variant="ghost" aria-label="Run">
<PlayIcon className="size-4" />
</Button>
</NodeAction>
</NodeHeader>
<NodeContent>{props.data.prompt}</NodeContent>
<NodeFooter>{props.data.tokenCount} tokens</NodeFooter>
</Node>Node with selection-anchored toolbar
Combine with Toolbar for actions that only show up when the node is selected.
<Node {...props} handles={{ target: true, 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>{props.data.label}</NodeContent>
</Node>Accessibility
- Keyboard: xyflow handles node selection and arrow-key movement when the canvas has focus. Interactive children (
NodeAction,Toolbarbuttons) need their ownaria-labels since they are typically icon-only. - Semantics: A
Noderenders as a<div>(viaCard). When the node represents something semantically important (an agent, a tool), expose that role viaaria-labelon theCarditself or onNodeTitle. - Handles: xyflow's
Handleis a small target circle without an accessible name. If your flow is meant to be navigated with assistive tech, supplyaria-labels describing what kind of connection the handle accepts ("Accepts message input"). - Color contrast: The secondary background on
NodeHeaderandNodeFooterclears 4.5:1 against the foreground token in both Gremorie themes. Re-verify if you customize tokens. - Focus visible: Do not strip the default xyflow focus ring on selected nodes - users on keyboard navigation rely on it.
Related
- Canvas - the surface where nodes live
- Edge - the lines that connect node handles
- Connection - the in-progress drag line between handles
- Toolbar - selection-anchored action bar for a node
- Panel - canvas-corner overlay for non-node-specific actions