Skip to main content

Props

<ForceGraph<TNode, TEdge>> accepts the following props. TNode must extend BaseNode { id: string } and TEdge must extend BaseEdge { id: string; source: string; target: string }.
nodes
TNode[]
required
Array of nodes to render. Each must have a unique id.
edges
TEdge[]
required
Array of edges connecting nodes by their id.
getNodeLabel
(node: TNode) => string
default:"n.label ?? n.name ?? n.id"
Text rendered below each node.
getNodeWeight
(node: TNode) => number
default:"n.weight ?? 0"
Drives degree-based collision radius and clustering hub priority.
getEdgeLabel
(edge: TEdge) => string
default:"e.label ?? \"\""
Text rendered on the midpoint of the edge. An empty string hides the label.
onNodeClick
(node: TNode) => void
Fires after the camera centres on the node.
onEdgeClick
(edge: TEdge) => void
Fires after the camera centres on the edge midpoint.
onBackgroundClick
() => void
Fires on canvas background click.
theme
Partial<GraphTheme>
default:"DEFAULT_THEME"
Deep-merged with defaults. See Theme.
disableClustering
boolean
default:"false"
Skip Louvain detection and the cluster-aware forces. The rest of the simulation (charge, collision, link spring) is untouched.
showZoomLevel
false | "top-right" | "top-left" | "bottom-right" | "bottom-left"
default:"false"
Render a live zoom-percentage indicator at the given corner.
className
string
default:"\"cg-root\""
Replaces the container class entirely.
aria-label
string
default:"\"Knowledge graph\""
Container aria-label.
emptyState
ReactNode
default:"\"No entities to display\""
Rendered when nodes is empty and isLoading is falsy.
loadingState
ReactNode
Rendered while the canvas lib is dynamic-imported, or when nodes is empty and isLoading is true.
isLoading
boolean
Sets aria-busy on the container.
ref
Ref<ForceGraphHandle>
Imperative handle — see below.

Imperative handle

Grab a ref to drive the graph from outside React state.
import { useRef } from "react";
import { ForceGraph, type ForceGraphHandle } from "@crosmos/graph";

const ref = useRef<ForceGraphHandle>(null);

<ForceGraph ref={ref} nodes={nodes} edges={edges} />;
zoom
(scale: number, durationMs?: number) => void
Animate to an absolute zoom level.
zoomToFit
(durationMs?: number, paddingPx?: number) => void
Fit every node in view.
centerAt
(x: number, y: number, durationMs?: number) => void
Pan to graph coordinates.
pauseAnimation
() => void
Stop the simulation.
resumeAnimation
() => void
Resume the simulation.
refresh
() => void
Force a single canvas repaint.

Clustering

Enabled by default. On each nodes or edges change, the renderer runs Louvain community detection and feeds the result into the simulation as:
  • Variable link distance — intra-community links pull to theme.cluster.intraLinkDistance (120 by default); inter-community bridges pull to theme.cluster.interLinkDistance (280).
  • Centroid force — every tick each node is nudged toward its community’s centroid at strength theme.cluster.strength · alpha.
Pass disableClustering to turn the whole thing off. Useful when you already have a layout, but the result is a single gravity-well that often looks crowded for graphs with natural communities.
For 500+ node graphs that still feel cramped at high zoom, the right knobs to reach for are force.chargeStrength (more negative = more repulsion), cluster.strength (lower = looser clusters), and node.radius (bigger collision floor). See Theme for ranges.
Bundle impact: graphology + graphology-communities-louvain add ~60 KB gzipped. Both are bundled into the package — no peer dependency to install.

Data

The package does no fetching. Pull data with whatever you already use (TanStack Query, SWR, RSC, plain fetch) and pass arrays straight in.
const { data } = useQuery({ queryKey: ["graph"], queryFn: loadGraph });

<ForceGraph nodes={data?.nodes ?? []} edges={data?.edges ?? []} />;
TNode must extend BaseNode { id: string }; TEdge must extend BaseEdge { id: string; source: string; target: string }. Carry any domain fields on the node or edge directly.

Sub-path exports

Import pathWhat’s there
@crosmos/graphForceGraph, BaseNode, BaseEdge, theme types, DEFAULT_THEME, mergeTheme.
@crosmos/graph/mock500-node mock dataset.
@crosmos/graph/styles.cssContainer and zoom-indicator default styles.

SSR and Next.js

The renderer wraps react-force-graph-2d, which is canvas-based and client-only. <ForceGraph> is marked "use client" and dynamic-imports the lib internally, so it’s safe to mount inside Next.js server components — the container renders its loadingState (or nothing) on the server and hydrates on the client.
Don’t import @crosmos/graph from a server-only module. It needs the browser to actually draw.