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.

ABCDE
Customize
Graph
5
80
Mode

Installation

npx shadcn@latest add https://craftbits.dev/r/edge-toggle.json

Usage

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

  1. 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.
  2. Edge slots. When editable is 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.
  3. 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.
  4. 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.
  5. Controlled or uncontrolled. Pass edges + onEdgesChange for the canonical Radix-controlled mode. Pass defaultEdges to let the component own the edge list internally.
  6. Reduced motion. When prefers-reduced-motion: reduce is set, every slot transition and node enter snaps to instant.

Props

PropTypeDefaultDescription
nodesreadonly string[]requiredOrdered node IDs. Placed uniformly around the ring.
nodeLabelsRecord<string, string>Per-node display label override.
edgesreadonly EdgeTuple[]Controlled edge set. Pair with onEdgesChange.
defaultEdgesreadonly EdgeTuple[][]Uncontrolled initial edge set.
onEdgesChange(next: EdgeTuple[]) => voidFires with the next edge set on toggle.
directedbooleanfalseRender arrowheads and treat [A, B] as distinct from [B, A].
editablebooleantrueShow every slot. When false, only present edges render.
ringRadiusnumber80Ring radius in px.
transitionTransitionSPRINGS.smoothSlot / edge transition.
overlayReactNodeSVG content rendered above edges, beneath nodes.
classNamestringMerged onto the outer SVG via cn().

Accessibility

  • The outer SVG is role="img" with an aria-label summarising the graph state (for example, "Undirected edge editor: 5 nodes, 2 edges.").
  • Every editable slot is role="checkbox" with aria-checked reflecting 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 a nodes + edges graph 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 to CycleRingViz and DAGRenderer without visual drift.