Graph view
ProA directed graph drawn from nodes that name what they come after, laid out on its own (layers, then an order that crosses few arrows; lib/graph-layout). Nodes are in an ordered list in the order given, so a screen reader reads them as a list, and the arrows are drawn only: say what they mean in each node's content. One node is in the tab order at a time; arrow keys move along the arrows and across a layer, mirrored right to left, and Home and End go to the first and last. With onSelect each node is a button.
Plan and RunTree use it for their graph view, and Diagram for diagrams given as nodes and edges. GraphViewSwitch is the List / Graph control they show when the work branches.
With places, nodes can be moved to the places your app names for them (between two nodes, into a branch), and the layout stays automatic: your app moves the node in its data and the graph is laid out again. With a mouse or a pen, a node is dragged: its places are drawn, a line and the place's label mark the one nearest the pointer, letting go calls onPlace, and Escape puts it back; near the graph's edge it scrolls that way. Farther than a node's width from every place, none is marked, and letting go there puts it back and says so. From the keyboard your app gives the control (a Move button that picks the node up, and arrow keys that walk its places) and draws it through moving, as WorkflowEditor does. On a touch screen a press on a node scrolls the graph as before; while your app draws a move through moving, each place is a button named by its label, out of the tab order, and a tap on one calls onPlace, so the same Move control works there. Give it a way to cancel too, since there is no Escape on a touch screen.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0 and 2.8.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/graph-view
Brings in: bidi, button, graph-layout, layer, locale, motion, tokens, use-announcer, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { GraphView, GraphViewSwitch, type GraphViewNode } from "@/components/candor/graph-view"
const step = (title: string, who: string, after?: string) => (
<span className="flex min-w-0 flex-col">
<span className="line-clamp-2 text-sm font-medium leading-snug">{title}</span>
<span className="text-xs text-muted-foreground">{who}</span>
{/* The arrows are drawn only, so each node says what it comes after. */}
{after && <span className="sr-only">. After {after}</span>}
</span>
)
const nodes: GraphViewNode[] = [
{ id: "a", content: step("Add the region column", "Dev agent") },
{ id: "b", after: ["a"], content: step("Show it on the customer page", "Dev agent", "Add the region column") },
{ id: "c", after: ["a"], content: step("Backfill from billing country", "Dev agent", "Add the region column") },
{ id: "d", after: ["b", "c"], content: step("Test on Staging", "QA agent", "Show it on the customer page and Backfill from billing country") },
]
export default function Example() {
const [view, setView] = React.useState<"list" | "graph">("graph")
const [active, setActive] = React.useState<string>()
return (
<div className="flex max-w-xl flex-col gap-2 rounded-xl border bg-card p-3">
<GraphViewSwitch view={view} onViewChange={setView} className="self-end" />
{view === "graph" ? (
<GraphView nodes={nodes} label="Plan steps as a graph" description="The same steps as the list, in the same order." onSelect={setActive} activeId={active} />
) : (
<ol className="list-decimal ps-6 text-sm">
{nodes.map((n) => (
<li key={n.id}>{n.content}</li>
))}
</ol>
)}
</div>
)
}<GraphView>
| Prop | Type | Default | Description |
|---|---|---|---|
| nodesrequired | GraphViewNode[] | — | The nodes, in reading order: the order a screen reader and Home and End follow. |
| labelrequired | string | — | The graph's accessible name. |
| description | string | undefined | — | A sentence read with the graph before the keys, e.g. "The same steps as the list, in the same order." |
| edgeLabels | GraphViewEdgeLabel[] | undefined | — | Words drawn on arrows. |
| direction | "down" | "across" | undefined | "down" | "down" (the default): layers are rows, top to bottom. "across": layers are columns, from the start side. |
| nodeWidth | number | undefined | 13 | Each node's width, in rem. Defaults to 13. |
| nodeHeight | number | undefined | 4.5 | Each node's height, in rem; longer content is clipped, so keep it short. Defaults to 4.5. |
| onSelect | ((id: string) => void) | undefined | — | Makes each node a button; called with its id. |
| activeId | string | undefined | — | The node shown as selected, when the app holds it. |
| defaultActiveId | string | null | undefined | — | The node selected at first, when the graph holds the selection itself: a node's id, or `null` for none. Given, a node the person selects is shown as selected, and `onSelect` is still called. Left out (and `activeId` too), no node is shown as selected, as before 5.1. |
| places | ((id: string) => GraphViewPlace[]) | undefined | — | The places a node can be moved to, asked for with its id; an empty list for a node that stays where it is. Given, a node with places can be dragged to one with a mouse or a pen. Left out, nothing moves, as before 4.5. |
| onPlace | ((id: string, place: string) => void) | undefined | — | Called when a dragged node is let go over one of its places, or a place is tapped or clicked while `moving`, with the node's id and the place's. End the move (clear `moving`) when it is called. |
| moving | { id: string; place: string } | undefined | — | A node your app is moving from the keyboard, and the id of the place it is at now: the node is drawn lifted, its places are drawn, and a line and the place's label mark that one, scrolled into view. Clear it when the move ends. |
| className | string | undefined | — | Extra classes for the outer element. |
<GraphViewSwitch>
The List / Graph control a component shows when its work branches: two buttons, the one in use pressed.
| Prop | Type | Default | Description |
|---|---|---|---|
| viewrequired | "list" | "graph" | — | The view in use. |
| onViewChangerequired | (view: "list" | "graph") => void | — | Called with the view the person picks. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under graphView in <LocaleProvider strings={{ … }}>; the table is exported as graphViewStrings.
| Key | English |
|---|---|
| keys | Arrow keys follow the arrows and move across each row; Home and End go to the first and the last. |
| view | View |
| list | List |
| graph | Graph |
| places | Places to move it to |
| putBack | Not moved: let go away from its places, so it is back where it was. |
Motion
| Moment | Family | What it does |
|---|---|---|
| settle | move | After a drop on a place, the nodes that changed place slide from where they were drawn to the new layout; the arrows are drawn there at once. A graph your app changes is drawn in its new layout. An arrow key pressed during the slide ends it. Under reduced motion the nodes are simply in their new places. |
| tint | tint | A selectable node's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus. Hovering moves nothing: the arrows and the nodes stay where they are drawn. |
Set with --cd-motion-graph-view-<moment>-* in your brand file; the families are in the design reference.
Types
- GraphViewNode
- A node in the graph: what it shows, and the nodes it comes after.
{ id: string; /** What the node shows. Put what the arrows say in words here too (`sr-only`), since the arrows are only drawn. */ content: React.ReactNode; /** The nodes this one comes after: an arrow from each to it. */ after?: string[] | undefined; /** A fixed layer: the row going down, the column going across. Left out, it's laid out from `after`. */ layer?: number | undefined; /** A fixed place across its layer. Left out, it's laid out to cross as few arrows as it can. */ slot?: number | undefined; /** "muted" for a node that's out of the work (skipped, say). */ tone?: "default" | "muted" | undefined; }- GraphViewEdgeLabel
- Words drawn on an arrow. Drawn only: say them in the target's or source's `content` as well.
{ from: string; to: string; label: string }- GraphViewPlace
- A place a node can be moved to, in the layout's own units, as a node's `layer` and `slot`: a fraction is between two, so layer 1.5 is midway between the second layer and the third. It is drawn where a node there would have its middle.
{ id: string; /** What the place is, in words ("2 of 3 in Then of If Priority: High"): drawn by its line while it is chosen. */ label: string; /** The layer it is in, as a node's; a fraction is between two layers. */ layer: number; /** Where it is across its layer, as a node's slot; a fraction is between two. */ slot: number; }