Edge Toggle
A tiny editable directed/undirected graph laid out on a circle. Nodes sit at uniform angles around the perimeter and every pair of nodes exposes a clickable edge slot — tap a slot to flip the edge on or off. Designed for graph-mutation narratives where the felt point is edges come and go: cycle creation, MST building, bipartite-test setup, dependency wiring.
EdgeToggle is a layout + state primitive. It does not run any graph algorithm — the caller drives the edge list (or hands it off to the component via defaultEdges), and the algorithm runs in surrounding lesson code.
Installation
npx shadcn@latest add https://craftbits.dev/r/edge-toggle.jsonUsage
import { EdgeToggle, type EdgeTuple } from "@craft-bits/core";
const [edges, setEdges] = useState<EdgeTuple[]>([["A", "B"]]);
<EdgeToggle
nodes={["A", "B", "C", "D"]}
edges={edges}
onEdgesChange={setEdges}
/>;Directed mode — [A, B] and [B, A] become distinct edges and the visible line gains an arrowhead:
<EdgeToggle
nodes={["A", "B", "C", "D"]}
directed
defaultEdges={[["A", "B"], ["B", "C"]]}
/>Uncontrolled — let the component own the edge list internally:
<EdgeToggle nodes={["X", "Y", "Z"]} defaultEdges={[["X", "Y"]]} />Read-only mode — render only the present edges, no slots for absent pairs:
<EdgeToggle
nodes={["A", "B", "C", "D"]}
edges={[["A", "B"], ["B", "C"]]}
editable={false}
/>Understanding the component
- Polar layout. Nodes are placed at angles starting from the top of the ring and stepping clockwise. The ring radius defaults to 80 px and can be overridden via
ringRadius. - Edge slots. When
editableis true (the default), every pair of distinct nodes gets a slot — n*(n-1)/2 in the undirected case, n*(n-1) in the directed case. Present edges render with the accent stroke; absent slots render as a dashed ghost line. - Canonical edge keys. Undirected edges canonicalise to the sorted-pair form, so the component never emits both
[A, B]and[B, A]for the same undirected pair. Directed edges keep the input order. - 44 px hit target. Each editable slot has an invisible 44 px stroke overlay (per WCAG 2.5.8) so even short edges between adjacent nodes are tappable on touch.
- Controlled or uncontrolled. Pass
edges+onEdgesChangefor the canonical Radix-controlled mode. PassdefaultEdgesto let the component own the edge list internally. - Reduced motion. When
prefers-reduced-motion: reduceis set, every slot transition and node enter snaps to instant.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | readonly string[] | required | Ordered node IDs. Placed uniformly around the ring. |
nodeLabels | Record<string, string> | — | Per-node display label override. |
edges | readonly EdgeTuple[] | — | Controlled edge set. Pair with onEdgesChange. |
defaultEdges | readonly EdgeTuple[] | [] | Uncontrolled initial edge set. |
onEdgesChange | (next: EdgeTuple[]) => void | — | Fires with the next edge set on toggle. |
directed | boolean | false | Render arrowheads and treat [A, B] as distinct from [B, A]. |
editable | boolean | true | Show every slot. When false, only present edges render. |
ringRadius | number | 80 | Ring radius in px. |
transition | Transition | SPRINGS.smooth | Slot / edge transition. |
overlay | ReactNode | — | SVG content rendered above edges, beneath nodes. |
className | string | — | Merged onto the outer SVG via cn(). |
Accessibility
- The outer SVG is
role="img"with anaria-labelsummarising the graph state (for example, "Undirected edge editor: 5 nodes, 2 edges."). - Every editable slot is
role="checkbox"witharia-checkedreflecting the present/absent state. Slots are focusable; Space and Enter toggle the edge. - Slots expose
data-state="on"/data-state="off"so consumer apps can hook custom styles. - Each editable slot has a 44 px invisible hit stroke (per WCAG 2.5.8) so even short edges between adjacent nodes meet the minimum touch target.
- Present vs. absent edges layer dash + stroke width + colour, never colour alone — the distinction stays legible for colour-blind users.
- Motion respects
prefers-reduced-motion: reduce— slot transitions and node enters collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/EdgeToggle.tsx). The original source path was empty — the library version is implemented from concept. It generalises to anodes+edgesgraph state on the Radix controlled/uncontrolled pattern, supports both directed and undirected modes, and uses the shared SVG token set (SVG_TOKENS,SvgDefs,arrowEndpoint,edgeEndpoints) so it sits next toCycleRingVizandDAGRendererwithout visual drift.