Knowledge Graph
An undirected-graph layout primitive — circular nodes joined by symmetric links, with deterministic radial auto-layout and click-to-focus neighbourhood highlighting. Designed for concept maps, design-principle webs, knowledge bases, and any picture where the story is the local neighbourhood, not a directed flow.
KnowledgeGraph is a pure layout primitive. Pass flat nodes and edges lists; the component lays them out and lights up whichever node you click. Distinct from DepGraphViz, which renders a directed dependency flow lit by an external highlight path.
Installation
npx shadcn@latest add https://craftbits.dev/r/knowledge-graph.jsonUsage
import { KnowledgeGraph } from "@craft-bits/core";
const nodes = [
{ id: "hierarchy", label: "Hierarchy" },
{ id: "contrast", label: "Contrast" },
{ id: "rhythm", label: "Rhythm" },
];
const edges = [
{ source: "hierarchy", target: "contrast" },
{ source: "hierarchy", target: "rhythm" },
];
<KnowledgeGraph nodes={nodes} edges={edges} />;Controlled focus — the parent owns which node is highlighted:
const [focusedId, setFocusedId] = useState<string | null>("hierarchy");
<KnowledgeGraph
nodes={nodes}
edges={edges}
focusedId={focusedId}
onFocusedIdChange={setFocusedId}
/>;Caller-driven coordinates — every node carries its own x and y, the component just draws:
<KnowledgeGraph
nodes={[
{ id: "A", label: "A", x: 80, y: 80 },
{ id: "B", label: "B", x: 240, y: 80 },
{ id: "C", label: "C", x: 160, y: 200 },
]}
edges={[
{ source: "A", target: "B" },
{ source: "B", target: "C" },
]}
/>;Category-tinted strokes — map each node.category to a stroke colour:
<KnowledgeGraph
nodes={nodes}
edges={edges}
categoryColor={{
structure: "var(--cb-accent)",
motion: "var(--cb-fg-muted)",
}}
/>;Understanding the component
- Two layout modes. If every node carries explicit
xandy, the component trusts the caller's coordinates — useful when the parent has already chosen a hand-tuned scene or is running its own force simulation. Otherwise it falls back to a deterministic radial layout: nodes with degree at least 3 are treated as hubs and placed on an inner ring, every other node sits on the outer ring half a step offset so leaves don't eclipse hubs. The ordering is insertion-stable, so SSR and client agree. - Undirected edges. Edges are shortened symmetrically at both ends via the shared
edgeEndpointshelper, so the link stops cleanly at each node's circular boundary. There are no arrowheads — direction is not part of the story. - Click-to-focus the local neighbourhood. Clicking a node toggles it as the focused node; clicking the same node again, or clicking the empty background, clears the focus. While focused, the focused node and its one-hop neighbours render at full opacity with the accent stroke; everything else fades to context. Edges touching the focused node light up; the rest dim. The component supports both controlled (
focusedId+onFocusedIdChange) and uncontrolled (defaultFocusedId) usage in the Radix style. - Token-driven styling. Node radius defaults to 20 px; focused fills resolve to
var(--cb-accent-muted), focused strokes tovar(--cb-accent), and rest fills tovar(--cb-bg-elevated). Hex never leaks into the component body — themes drop in via CSS variables. Per-category accent tones are opt-in via thecategoryColorprop. - Reduced motion.
usePrefersReducedMotion()short-circuits node entry, edge draw, and focus dimming transitions to instant. The graph snaps into place without any path-length animation.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | readonly KnowledgeGraphNode[] | required | Each node has a stable id, optional label, optional category, and optional x / y coordinates. |
edges | readonly KnowledgeGraphEdge[] | required | Undirected edges with source and target node ids. |
focusedId | string | null | — | Controlled focused node id. Pair with onFocusedIdChange. |
defaultFocusedId | string | null | null | Uncontrolled initial focused node. |
onFocusedIdChange | (id: string | null) => void | — | Fires when the focused node changes. |
categoryColor | Record<string, string> | — | Map of node.category to stroke colour. Unmatched categories fall back to var(--cb-accent). |
nodeRadius | number | 20 | Node circle radius in px. |
innerRingRadius | number | 90 | Inner hub ring radius (auto-layout only). |
outerRingRadius | number | 180 | Outer leaf ring radius (auto-layout only). |
transition | Transition | SPRINGS.smooth | Override the spring used for enter transitions. |
className | string | — | Merged onto the outer SVG element. |
Accessibility
- The outer SVG is
role="img"with anaria-labelsummarising node and edge counts (for example, "Knowledge graph: 8 nodes, 9 edges."). - Each node renders as
role="button"withtabIndex={0},aria-labelmatching the node label, andaria-pressedreflecting whether it is the focused node.EnterandSpacetoggle focus from the keyboard. - Highlighted nodes and edges carry both an opacity shift and a scale bump, so the focused neighbourhood survives high-contrast and grayscale rendering. Identity is communicated through the on-circle label, never through colour alone.
- Motion respects
prefers-reduced-motion: node entries, edge draws, focus dimming, and stroke-colour transitions all collapse to instant.
Credits
- Extracted from:
terminal-dreams(src/components/principles/KnowledgeGraph.tsx). The original was scoped to the design-principles index — it pulled the principle list and category palette from project singletons, ran ad3-forcesimulation in auseEffect, rendered a mobile-only HTML list fallback, and routed every node click throughnext/linkto the principle page. The library extract drops thed3-forcedependency in favour of a deterministic radial fall-back (degree-1 leaves on an outer ring, hubs on an inner ring), drops the mobile-list fallback and Next-specific link wiring, generalises the API to flatnodesandedgeslists with optional caller-driven coordinates, switches the active-category prop to a Radix-pattern controlled-or-uncontrolledfocusedIdso the click-to-focus interaction works without any host context, and routes every dimension throughSVG_TOKENSso it sits next toDepGraphVizandForestVizwithout visual drift.