Styling

FlowDiagram draws React Flow's default theme. There are three levels of control:

  1. config changes tokens: colours, sizes, the background, the controls and the minimap.
  2. custom, nodeTypes and edgeTypes replace individual parts with your own widgets.
  3. The headless Flow widget drops the React Flow look entirely and keeps the behaviour.

Config

config is a deep partial of XyflowFlowConfig. It is merged over the defaults, so set only what you change. colorMode picks the light or dark palette first, then colors overrides single tokens.

FlowDiagram({
  nodes,
  edges,
  config: {
    colorMode: "dark",
    colors: { background: "#0d0f13", edgeStroke: "#4b5563" },
    background: { variant: "lines", gap: 24 },
    controls: { position: "top-left", showInteractive: false },
    minimap: { visible: false },
  },
});

To follow a site theme, rebuild the widget with the other colorMode when the theme changes. createXyflowConfig(partial) returns the fully merged config if you need to read the resolved values.

Colours

Values mirror React Flow's style.css custom properties.

TokenLightDark
background#ffffff#141414
patternDots#91919a#777777
patternLines#eeeeee#777777
patternCross#e2e2e2#777777
nodeBackground#ffffff#1e1e1e
nodeGroupBackgroundrgba(240, 240, 240, 0.25)rgba(240, 240, 240, 0.25)
nodeBorder#1a192b#3c3c3c
nodeSelectedBorder#1a192b#999999
nodeColor#222222#f8f8f8
nodeHoverShadowrgba(0, 0, 0, 0.08)rgba(255, 255, 255, 0.08)
handleBackground#1a192b#bebebe
handleBorder#ffffff#1e1e1e
handleValid#55dd99#55dd99
handleInvalid#ff0072#ff0072
edgeStroke#b1b1b7#3e3e3e
edgeStrokeSelected#555555#727272
edgeStrokeUpdating#777777#777777
edgeLabelBackground#ffffff#141414
edgeLabelColor#222222#f8f8f8
connectionLine#b1b1b7#b1b1b7
selectionBackgroundrgba(0, 89, 220, 0.08)rgba(200, 200, 220, 0.08)
selectionBorderrgba(0, 89, 220, 0.8)rgba(200, 200, 220, 0.8)
controlsBackground#fefefe#2b2b2b
controlsBackgroundHover#f4f4f4#3e3e3e
controlsColor#222222#f8f8f8
controlsBorder#eeeeee#5b5b5b
controlsShadowrgba(0, 0, 0, 0.08)rgba(0, 0, 0, 0.08)
minimapBackground#ffffff#141414
minimapMaskrgba(240, 240, 240, 0.6)rgba(60, 60, 60, 0.6)
minimapNode#e2e2e2#2b2b2b
minimapViewportStroketransparenttransparent

The palettes are exported as xyflowLightColors and xyflowDarkColors.

Node, handle and edge

TokenDefaultDescription
node.width150Width of built-in nodes without a width
node.padding10Inner padding
node.borderRadius3Corner radius
node.borderWidth1Border width; selected nodes add 0.5
node.fontSize12Label size
node.fontFamilySystem UI stackLabel and edge label font
handle.size6Handle diameter. FlowDiagram also uses it as the handleSize option.
handle.borderWidth1Handle outline
handle.hitPadding4Transparent margin around connectable handles
edge.strokeWidth1Edge stroke
edge.curvature0.25Bézier curvature
edge.stepOffset20Distance a step edge travels before its first turn
edge.stepBorderRadius5Corner radius of smoothstep edges
edge.markerColornullMarker colour; null uses the edge stroke
edge.markerSize12.5Default marker width and height in pixels
edge.animatedDash5Dash length of animated edges
edge.animationDuration500Milliseconds per dash cycle
edgeLabel.fontSize10Label size
edgeLabel.paddingX, edgeLabel.paddingY4, 2Label padding
edgeLabel.borderRadius2Label corner radius
edgeLabel.showBackgroundtrueDraw colors.edgeLabelBackground behind the label
connectionLine.strokeWidth1Stroke of the line drawn while connecting
selection.dash1Dash length of the selection rectangle outline
selection.borderWidth1Outline width of the selection rectangle

Resize controls

The controls drawn around a selected resizable node, React Flow's NodeResizer look.

TokenDefaultDescription
resize.color#3367d9Colour of the lines and handles
resize.handleSize4Size of the square handles
resize.lineWidth1Width of the side lines
resize.hitPadding4Transparent margin that still grabs a line or handle

Background

TokenDefaultDescription
background.visibletruefalse leaves only the pane colour
background.variant"dots""dots", "lines" or "cross"
background.gap20Pattern spacing in flow units; it scales with zoom
background.sizenullDot diameter (default 1) or cross length (default 6). Not used by "lines".
background.lineWidth1Stroke width for "lines" and "cross"
background.colornullPattern colour; null uses patternDots, patternLines or patternCross

The pattern follows the viewport, so it moves and scales with the graph.

Controls

The controls panel holds zoom in, zoom out, fit view and a lock that toggles controller.setInteractive().

TokenDefaultDescription
controls.visibletrueShow the panel
controls.position"bottom-left"A PanelPosition
controls.showZoomtrueZoom in and out buttons
controls.showFitViewtrueFit view button
controls.showInteractivetrueLock button
controls.buttonSize26Button size in pixels
controls.iconSize12Icon size in pixels
controls.fitViewDuration200Fit animation in milliseconds
controls.zoomDuration0Zoom animation in milliseconds

Minimap

The minimap draws every visible node and masks the area outside the viewport. Drag it to pan.

TokenDefaultDescription
minimap.visibletrueShow the minimap
minimap.position"bottom-right"A PanelPosition
minimap.width, minimap.height200, 150Size in pixels
minimap.pannabletrueDragging the minimap pans the diagram
minimap.nodeBorderRadius5Corner radius of node shapes
minimap.offsetScale5Padding around the content
minimap.nodeColornullNode fill; null uses colors.minimapNode
minimap.maskColornullMask fill; null uses colors.minimapMask

Panels

panels pins widgets over the pane, 15px from its edge. Inside a panel, FlowProvider.of(context) returns the controller, and the panel rebuilds whenever the diagram changes, including every frame of a pan or drag. Keep panels light.

import {
  type BuildContext, Container, EdgeInsets, StatelessWidget, Text, TextStyle, type Widget,
} from "flitter-core";
import { FlowDiagram, FlowProvider } from "flitter-diagram";
 
class ZoomLabelWidget extends StatelessWidget {
  build(context: BuildContext): Widget {
    const flow = FlowProvider.of(context);
    return Container({
      padding: EdgeInsets.symmetric({ horizontal: 8, vertical: 4 }),
      color: "#ffffff",
      child: Text(`${Math.round(flow.getZoom() * 100)}%`, { style: new TextStyle({ fontSize: 12 }) }),
    });
  }
}
 
function ZoomLabel(): Widget {
  return new ZoomLabelWidget();
}
 
FlowDiagram({
  nodes,
  edges,
  panels: [{ position: "top-right", child: ZoomLabel() }],
});

XyflowControls() and XyflowMiniMap() are panels too. FlowDiagram adds them from config.controls and config.minimap, before your own panels.

Custom slots

custom replaces any rendering slot. FlowDiagram fills the slots you leave out with React Flow's look. Each builder receives its arguments and a context, which is the controller with config attached.

SlotArgumentsReturns
nodeNodeBuilderArgsFallback node widget when nodeTypes has no match
edgeEdgeBuilderArgsFallback edge widget when edgeTypes has no match
edgeLabel{ edge, label, selected, hovered, x, y }Label widget, or null to hide it
handleHandleBuilderArgsHandle widget, centred on the handle
defaultHandles(node)HandleSpec[] when neither the node nor its type declares handles
connectionLine{ connection, path, from, to, isValid, size, … }The line drawn while connecting
background{ viewport, width, height }Pane background, or null
selectionBox{ rect }The box drawn during box selection, in pane pixels
nodesSelectionRect{ rect, nodes }The frame around a multi-node selection, or null
edgePath{ edge, params, type }EdgePathResult for built-in edge types and the connection line
resizeControl{ node, position, variant, resizing }One resize line (variant: "line") or handle ("handle"). Its size is the grab area.
reconnectHandle{ edge, end, size }The grab area at an edge end, size units across. Invisible by default.

A resize line is stretched along its side: top and bottom lines get the node's width, left and right lines its height. Give it a thickness on the other axis. A handle is centred on its corner or side.

The defaults are exported as xyflowCustom and one by one: xyflowNode, xyflowEdge, xyflowEdgeLabel, xyflowHandle, xyflowDefaultHandles, xyflowConnectionLine, xyflowBackground, xyflowSelectionBox, xyflowNodesSelectionRect, xyflowEdgePath, xyflowResizeControl and xyflowReconnectHandle. The built-in type maps are xyflowNodeTypes and xyflowEdgeTypes.

The editor wires all gestures itself. Builders should read selected, hovered, dragging and the connection flags from their arguments rather than add their own hover or click handlers.

For drawing, ShapePaint({ width, height, shapes }) paints named SVG path strings, and IconPaint({ d, viewBox, size, color }) paints a single icon path. Both produce the same result on SVG and Canvas. rectPathD(x, y, width, height, radius?) builds a rectangle path string.

Headless Flow

Flow is the engine without a look. It has the same behaviour, options and callbacks as FlowDiagram, with three differences:

  • custom is required and must fill every slot.
  • config is passed through untouched, typed as whatever your builders expect.
  • There are no built-in node types, edge types, controls or minimap. Add them through nodeTypes, edgeTypes and panels.
import { Border, BoxDecoration, Container, EdgeInsets, SizedBox, Text, TextStyle } from "flitter-core";
import {
  Flow,
  ShapePaint,
  commandsToD,
  getEdgePathByType,
  rectPathD,
  type FlowCustom,
  type HandleSpec,
} from "flitter-diagram";
 
type InkConfig = { ink: string; paper: string; accent: string };
 
const sideHandles: HandleSpec[] = [
  { type: "target", position: "left" },
  { type: "source", position: "right" },
];
 
const inkCustom: FlowCustom<InkConfig> = {
  node: ({ node, selected }, { config }) =>
    Container({
      padding: EdgeInsets.symmetric({ horizontal: 14, vertical: 8 }),
      decoration: new BoxDecoration({
        color: config.paper,
        border: Border.all({ color: selected ? config.accent : config.ink, width: 2 }),
      }),
      child: Text(String(node.data.label ?? node.id), {
        style: new TextStyle({ color: config.ink, fontSize: 13 }),
      }),
    }),
  edge: ({ path, size, selected }, { config }) =>
    ShapePaint({
      width: size.width,
      height: size.height,
      shapes: {
        line: { d: commandsToD(path.commands), stroke: selected ? config.accent : config.ink, strokeWidth: 2 },
      },
    }),
  edgeLabel: () => null,
  handle: ({ handle }, { config }) =>
    Container({ width: handle.width + 4, height: handle.height + 4, color: config.ink }),
  defaultHandles: () => sideHandles,
  connectionLine: ({ path, size }, { config }) =>
    ShapePaint({
      width: size.width,
      height: size.height,
      shapes: {
        line: { d: commandsToD(path.commands), stroke: config.accent, strokeWidth: 2, dash: [4, 4] },
      },
    }),
  background: ({ width, height }, { config }) => Container({ width, height, color: config.paper }),
  selectionBox: ({ rect }, { config }) =>
    ShapePaint({
      width: rect.width,
      height: rect.height,
      shapes: { box: { d: rectPathD(0, 0, rect.width, rect.height), stroke: config.accent, strokeWidth: 1 } },
    }),
  nodesSelectionRect: () => null,
  edgePath: ({ type, params }) => getEdgePathByType(type, params),
  // Lines are invisible 8px strips; handles are accent squares
  resizeControl: ({ variant }, { config }) =>
    variant === "line" ? SizedBox({ width: 8, height: 8 }) : Container({ width: 8, height: 8, color: config.accent }),
  reconnectHandle: ({ size }) => SizedBox({ width: size, height: size }),
};
 
export const inkDiagram = Flow<InkConfig>({
  nodes,
  edges,
  custom: inkCustom,
  config: { ink: "#1f2937", paper: "#fdfaf3", accent: "#d97706" },
  fitView: true,
});

To keep most of React Flow's look and change a few slots, spread xyflowCustom and pass a config from createXyflowConfig():

import { Flow, XyflowControls, createXyflowConfig, xyflowCustom, type XyflowFlowConfig } from "flitter-diagram";
 
Flow<XyflowFlowConfig>({
  nodes,
  edges,
  config: createXyflowConfig({ colorMode: "dark" }),
  custom: { ...xyflowCustom, nodesSelectionRect: () => null },
  panels: [{ position: "bottom-left", child: XyflowControls() }],
});

XyflowControls and XyflowMiniMap read XyflowFlowConfig tokens, so use them only with that config.