import { NetworkChart } from "@elabs-ai/components-charts";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-chartsPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui NetworkChart chart (from @elabs-ai/components-charts) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look NetworkChart up before writing code: `npx -y @elabs-ai/components-cli docs NetworkChart` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-charts is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Use it for
- How does it connect or flow? What connects to what.
- arbitrary node and edge relationships with no hierarchy
Avoid
- the relationship really is a hierarchy — use a tree chart or a treemap
- Reading a force layout's POSITIONS as data — only adjacency is encoded; distance, direction and the picture's orientation are artefacts of the solver's seed. Use layout="circular" or "arc" whenever position must mean something.
- Labelling every node past roughly 15 — set `labelThreshold` so only the nodes worth naming carry text; the datapoint targets already announce every node to a screen reader.
- Reaching for layout="arc" on a graph that is not bipartite — the two columns are DERIVED (exactly two groups, else the nodes that are only ever a link source go left), so a general graph draws a colonnade that asserts a split the data does not have.
- Turning `draggable` on and expecting the dragged position to persist — a node springs back on release because the settled layout is the answer; a pinned position would be a claim about the data that the layout never made.
- Passing thousands of nodes because `maxNodes` is only a warning — the force solve is synchronous, so past a few hundred nodes it blocks the frame that renders it. Aggregate into hubs before the chart, not inside it.
Works with
Used inside
Pairs with
Examples
Small Circular
**G6 — the small ring.** Twelve nodes, every one labelled, chords bundled toward the centre so a dense middle still reads. The ring is the right answer under ~15 nodes: position carries no meaning, so nothing is lost by fixing it, and the eye gets a stable place to look each node up.
Loading
`status="loading"` (RM-184): the skeleton + `ChartLoadingLabel`, until the data arrives.
Small Force
**G11 — the small force cloud.** The same graph, laid out by attraction and repulsion instead of by decree: clusters that talk to each other end up together, and the Data tier falls out on its own. The simulation does NOT tick. It is solved to a fixed 300-tick budget from a seeded start before React sees a coordinate, so the picture is identical on every render, in every process — and nothing moves under `prefers-reduced-motion`.
Big Circular
**B1 — the big ring.** Sixty nodes is past the point where every label fits, so `labelThreshold` names only the people worth naming; the rest stay dots. Hover or focus any node: it and its neighbours stay lit and everything else blurs, which is the only way a 140-chord ring answers "who does this one work with". The blur is a CSS class on each `<g>`, driven by ONE piece of state — no node re-renders on hover, which is what keeps it at 60 fps here and at 180 nodes.
Big Force
**B2 — the big force cloud.** 180 nodes, hubs and satellites. The whole layout is solved synchronously in well under the 500 ms budget, and it is the SAME layout every time.
Ownership
**L12 — the ownership colonnade.** Two columns, hairline béziers, and no node `value` at all: with nothing to size by, a node's radius follows its DEGREE, so a team that owns five services reads as a hub without anyone computing a number for it. The split is derived, not declared — exactly two groups puts one on each side; failing that, the nodes that are only ever a link's `source` go left.
Draggable
**Drag (force only).** Pull a node aside to look under a cluster; it springs back to its settled position on release, because the settled layout IS the answer and a dragged position would be a lie about the data. Dragging is a pointer affordance. The keyboard path sits beside it: one tab stop into the chart, arrows across every node, each target named with its degree. That path is mounted by `onDatapointClick` (or `copyValueOnActivate`), not by `draggable` — so this story sets a handler, and its play function proves the path is really there (#274).
Keyboard Drilldown
Every node is a keyboard datapoint target — a real `<button>` in a `pointer-events: none` layer BESIDE the `aria-hidden` SVG, never inside it. The target's own accessible name carries the node's degree, so the answer a sighted user reads in the tooltip reaches a screen-reader user too.
API
NetworkChart
| Prop | Type | Description |
|---|---|---|
messages | ChartMessages | messages group (RM-187): this chart's own words, keyed by the ui catalogue's `charts.*` message keys. A key set here wins over the `LocaleProvider`; every other key reads the catalogue as before. |
nodesrequired | NetworkNodeDatum[] | The graph's nodes. `id` is the identity `links` reference. |
linksrequired | NetworkLinkDatum[] | The graph's edges. An endpoint naming an unknown node is dropped (dev warning). |
layoutrequired | NetworkLayout | - `"force"` — a settled force-directed cloud (G11, B2). - `"circular"` — one ring, chords bundled toward the centre (G6, B1). - `"arc"` — bipartite ownership: two columns, hairline béziers (L12). |
nodeSize | "value" | number | `"value"` (default) — AREA is proportional to the node's weight, i.e. radius ∝ √weight. A number pins every node to that radius. "Weight" is the node's own `value`, or its DEGREE when it has none — which is what gives a colonnade "hub radius ∝ link count" for free. |
labelThreshold | number | Draw a node's label only when its weight reaches this (lieflat B1's rule for a 60-node ring). Omit to label every node — right for ≤ ~15 nodes, unreadable above it. |
emphasis | NetworkEmphasis | `"adjacency"` (default) — hovering or focusing a node keeps it and its neighbours lit and blurs the rest. `"none"` disables it. |
draggable | boolean | `force` only: let the pointer pull a node out of place. It springs back to its settled position on release — the layout is the answer, the drag is a way to look under a cluster. Ignored (with a dev warning) on the other two layouts, which have no free positions to give. |
palette | ChartPalette | Colour family. Default: `"categorical"` at or under six groups, `"mono"` above — past six, a hue per group names nothing. |
maxNodes | number | Dev-warning threshold on node count. Default 200. NOT a cap. |
seed | number | `force` only: changes the starting cloud, and so which arrangement is found. |
valueFormat | ChartValueFormat | How node values render in the tooltip. Default `"compact"`. |
className | string | |
style | CSSProperties | |
aspectRatio | string | Aspect ratio as "width / height". Default `"16 / 9"`. |
plotHeight | Responsive<ChartPlotHeight> | The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Wins over `aspectRatio`, which stays an alias. |
accessibleLabel | ChartA11yProps["accessibleLabel"] | Accessible name for the chart region. Defaults to the auto summary — `"Network, 60 nodes, 140 links, 5 groups"` — because the counts are exactly what an `aria-hidden` SVG withholds. |
accessibleDescription | ChartA11yProps["accessibleDescription"] | Supplemental description read by AT. |
status | ChartStatus | Show the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`. |
empty | ChartEmptyState | Title and message shown when there is nothing to plot. |
onDatapointClick | ChartDatapointClickHandler<NetworkDatapointDatum> | Fires when a datapoint is activated by pointer OR keyboard. Setting it is what enables the interaction layer — with it unset the chart renders exactly as before, with no extra DOM and no new focusables. |
copyValueOnActivate | boolean | Put the datapoint's EXACT value on the clipboard when it is activated — the recovery path for compact axis labels and tooltips, reachable by mouse AND keyboard because activation rides `ChartDatapointLayer`'s real buttons. Default `false` on a raw chart container: turning it on mounts the interaction layer, and the documented guarantee is that a chart with no interaction props renders byte-identical DOM. `AutoChart` — the spec-driven surface, where the caller never sees the axis config — turns it on by default. A consumer-supplied `onDatapointClick` always wins; the two never both run. |
datapointLabel | ChartDatapointLabel<NetworkDatapointDatum> | Override the accessible name of each keyboard target. |
maxInteractiveDatapoints | number | Dev-warning threshold on the number of keyboard targets. Default 500. NOT a functional cap — every plotted point stays reachable, so a keyboard user can reach exactly what a mouse user can click. |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| node | --chart-1 … --chart-12 by group via resolvePalette (categorical at or under 6 groups, mono above); the mark is stroked in the paper colour (--chart-background) so touching nodes still read apart |
| link | --chart-grid hairlines at 0.35 opacity, width scaled by the link's own value |
| emphasis | hover/focus dims non-neighbours to opacity 0.12 and non-incident links to 0.03 — a CLASS on each <g>, transitioned on the compositor, so no node re-renders per frame |
| label | HaloText in the chart-source type role, punched from var(--chart-background); drawn only at or above labelThreshold |
| focus | every node is a real button in ChartDatapointLayer beside the aria-hidden SVG (never inside it), carrying the shared compound focus indicator (--ring plus the --ring-contour outline), shown only on keyboard focus; one tab stop, arrows across the rest |