# Diagrams

_Box Kite 2.4.1 · a markdown copy of https://www.box-kite.dev/diagrams/_

A React Flow canvas in the page's theme, with no colour written anywhere: one container declares the variables xyflow reads, from the palette, in both themes.

## React Flow, in the page's theme

Box Kite is not a diagram library — the canvas stays React Flow's. What it adds is the part every app writes by hand: a dark mode, one selection colour, one edge grey, and a visible keyboard focus on the nodes you draw yourself. React Flow already reads about thirty CSS variables for its background, edges, handles, minimap and controls; `FlowContainer` is a Box that declares them from palette tokens, so the canvas below names no colour and switches with the theme in the header. Import React Flow's own stylesheet once, as its docs say: the container sets only the variables that stylesheet reads.

**A React Flow canvas themed by the page**

```jsx
import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react';
import { FlowContainer, flowColorMode } from '@box-kite/react/components/flow';

function Canvas() {
  const [theme] = Box.useTheme();

  return (
    <FlowContainer height={90}>
      <ReactFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={nodeTypes} colorMode={flowColorMode(theme)} fitView>
        <Background />
        <Controls />
        <MiniMap />
      </ReactFlow>
    </FlowContainer>
  );
}
```

## Eight roles, not thirty variables

Every `--xy-*` variable the container declares reads one of eight roles: `surface` (the canvas), `panel` (nodes, minimap, controls), `text`, `border`, `grid` (the background pattern), `edge`, `selection` (a selected edge, the connection line, the selection box, the resizer and your nodes' focus) and `handle`. The defaults are drawn in `base` and `accent`, so `Box.palette()` recolours a canvas with the rest of the page. `tokens` repaints a role — one token for both themes, or a pair — and a single variable is plain `vars`, because the container is a Box. A prop on `<MiniMap>` or `<Background>` still wins: that is the app speaking.

**Repaint a role — a custom node reads the same roles**

```jsx
<FlowContainer tokens={{ selection: 'emerald-500', surface: { light: 'base-50', dark: 'base-950' } }} p={4} borderRadius={3} css={{ backgroundColor: 'var(--flow-surface)' }}>
  <Svg viewBox="0 0 100 24" width="100%" height="4rem" label="Two steps joined by an edge, the second selected">
    <Path d="M34 12H66" stroke="var(--flow-edge)" strokeWidth={1} />
    <Rect x={4} y={4} width={30} height={16} rx={2} fill="var(--flow-panel)" stroke="var(--flow-border)" />
    <Rect x={66} y={4} width={30} height={16} rx={2} fill="var(--flow-panel)" stroke="var(--flow-selection)" />
  </Svg>
</FlowContainer>
```

## colorMode follows the page

React Flow's `colorMode="dark"` puts a `dark` class on its root, and Box Kite's themes treat that class as a theme root — so the two have to agree. Pass `colorMode={flowColorMode(theme)}` with the theme from `Box.useTheme()` and they always do. A canvas in a different theme from the page sits in `<Box.Theme use="local">`, which marks a boundary with `data-theme`. Never set `colorMode="light"` under a dark page: `light` is no boundary, so the page's dark rules still reach into the canvas and the two themes mix.

**A custom node with a visible focus and a selected state**

```jsx
import { Handle, NodeProps, Position } from '@xyflow/react';
import { flowNodeSelectedStyles, flowNodeStyles } from '@box-kite/react/components/flow';

function StepNode({ data, selected }: NodeProps) {
  return (
    <Box
      {...flowNodeStyles}
      {...(selected ? flowNodeSelectedStyles : {})}
      px={4}
      py={3}
      borderRadius={2}
      b={1}
      css={{ backgroundColor: 'var(--flow-panel)', borderColor: 'var(--flow-border)', color: 'var(--flow-text)' }}
    >
      <Handle type="target" position={Position.Left} />
      {String(data.label)}
      <Handle type="source" position={Position.Right} />
    </Box>
  );
}
```

## Focus and selection on your own nodes

xyflow removes the focus outline from every node and edge and gives a replacement only to its four built-in node types, so a custom node is invisible to a keyboard user. `flowNodeStyles` puts an outline on your node while xyflow's wrapper has `:focus-visible` — a `group` keyed on `react-flow__node` — and `flowNodeSelectedStyles` is the selected state, which xyflow marks with a class and hands your node as the `selected` prop. Both are outlines rather than shadows, so they survive forced colours, and their colour is the container's `selection` role. A custom edge built on `BaseEdge` is painted by xyflow's own selected rule; `flowEdgeStyles` widens it under Tab.

**A custom edge, wider under Tab**

```jsx
import { BaseEdge, EdgeProps, getSmoothStepPath } from '@xyflow/react';
import { useClassNames } from '@box-kite/react';
import { flowEdgeStyles } from '@box-kite/react/components/flow';

function StepEdge(props: EdgeProps) {
  const [path] = getSmoothStepPath(props);
  const { className, styles } = useClassNames(flowEdgeStyles, { svg: true, base: false });

  return (
    <>
      {styles}
      <BaseEdge path={path} markerEnd={props.markerEnd} className={className} />
    </>
  );
}
```

## Menus and popovers inside a node

A menu opened from a node is in the top layer, which changes where it paints and not where it sits in the tree: its presses, wheels and keys still bubble to the node, so Backspace on a menu item deletes the node and an arrow key moves it. Wrap the canvas in one `<LayerScope>` from `@box-kite/react/a11y` and every layer inside carries React Flow's opt-out classes. `isolate={['key']}` also keeps the layer's keys from reaching the page, which is what stops Safari treating a Backspace on a menu item as Back. The trigger itself sits in the node, so it carries `nodrag` of its own. Try it on the demo's three task nodes.

**One opt-out for every layer in the canvas**

```jsx
import { Edge, Node, ReactFlow } from '@xyflow/react';
import { LayerScope } from '@box-kite/react/a11y';

function Canvas({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) {
  return (
    <LayerScope className="nodrag nowheel nokey" isolate={['key']}>
      <ReactFlow nodes={nodes} edges={edges} />
    </LayerScope>
  );
}
```

## Classing React Flow's own elements

`useClassNames` puts Box props on an element Box does not render, and by default its class carries Box's reset: `margin: 0`, `display: block` and the base transition. On `<ReactFlow>`, `<MiniMap>` or a `BaseEdge` that undoes React Flow's own panel margin and transitions, so pass `{ base: false }` and only your props are added.

## A canvas that moves every frame

Every Box transitions over a quarter of a second, which is right for a button and wrong for a node dragged under the pointer: a Box given a new position or size every frame trails behind it. Name the transition on what moves (`transition="colors"`), or give the container `motion="static"`, which zeroes every Box transition inside the canvas — layers' entrances included, which is why it is opt-in.

## FlowContainer props

Everything below is this component’s own. All 238 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `tokens` | `FlowTokens` | — | The canvas's roles — `surface`, `panel`, `text`, `border`, `grid`, `edge`, `selection`, `handle` — each a token for both themes (`{ selection: 'sky-500' }`) or one per theme (`{ surface: { light: 'base-50', dark: 'base-950' } }`). One `--xy-*` variable is plain `vars`, because the container is a Box. |
| `motion` | `'static'` | — | `'static'` zeroes every Box transition inside the canvas, layers' entrances included — for a canvas whose nodes are Boxes moved or resized every frame, which the default `transition: all` would make trail the pointer. |

## FlowContainer accessibility

- A `<div>` with variables on it: no role, no name. React Flow owns the canvas's accessibility; a custom node or edge gets a visible focus from `flowNodeStyles`/`flowEdgeStyles`.

Swept with axe on every release, in this state: `FlowContainer`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## FlowContainer style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`flowContainer`

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

