Hexpunk
InstallConceptsElementsProsehp-cellhp-codehp-copyhp-hexhp-latexhp-visually-hiddenhp-buttonhp-checkboxhp-formhp-labelhp-radiohp-selecthp-sliderhp-togglehp-toggle-grouphp-badgehp-taghp-progresshp-spinnerhp-backgroundhp-clusterhp-collapsiblehp-demohp-gridhp-scroll-areahp-separatorhp-sidebarhp-toolbarhp-avatarhp-iconhp-pixelhp-linkhp-menubarhp-navigation-menuhp-tabshp-alert-dialoghp-context-menuhp-dialoghp-dropdown-menuhp-hover-cardhp-popoverhp-tooltiphp-bannerhp-toasthp-tetherhp-module-handlehp-unfold-listhp-unfold-overlayhp-unfold-pagehp-bondhp-link-nodePaletteAnimationsReleases

hp-tether

Arc-tether primitive — curved SVG bezier connecting two distant molecules. Drag any anchor; the arc tracks the move, reroutes around obstacles, and re-settles on snap. directed adds an arrowhead.

Examples API Intent Changelog

The classic shape: three hexes placed by axial coords, three <hp-tether> elements authored next to the grid pointing at the endpoints by selector. Drag any node — the arcs reroute around obstacles and re-settle on snap.

node a node b node c
<body> <hp-grid> <hp-cell variant="anchor" q="-2" r="0" id="node-a">node a</hp-cell> <hp-cell variant="anchor" q="2" r="0" id="node-b">node b</hp-cell> <hp-cell variant="anchor" q="0" r="-1" id="node-c">node c</hp-cell> </hp-grid> <hp-tether from="#node-a" to="#node-b"></hp-tether> <hp-tether from="#node-a" to="#node-c" state="idle"></hp-tether> <hp-tether from="#node-b" to="#node-c" directed></hp-tether> </body>

Same arc primitive, different authoring path. The grid carries tetherable, so dropping any hex onto another toggles an <hp-tether> arc between the pair — created with the draw-in animation, removed if it already exists. No manual tether authoring; the grid handles from / to selectors and dedup. Drop on an empty cell to move a hex normally. The target hex highlights green while the source is hovering over it.

a b c d <body> <!-- Authoring: just the grid + its hexes. hp-tether elements are auto-appended by the grid on drop. --> <hp-grid draggable tetherable> <hp-cell variant="anchor" q="-2" r="0">a</hp-cell> <hp-cell variant="anchor" q="2" r="0">b</hp-cell> <hp-cell variant="anchor" q="0" r="-2">c</hp-cell> <hp-cell variant="anchor" q="0" r="2">d</hp-cell> </hp-grid> document.querySelector("hp-grid") .addEventListener("hp-grid-tether", (ev) => { // ev.detail = { source, target, tether } // tether.drawIn() has already been called for you. }); </body>

Install

bun add @hexpunk/core lit
import "@hexpunk/core";

Properties

Property Attribute Type Default Description
from from string | undefined CSS selector for the source molecule. Resolved against document.
to to string | undefined CSS selector for the target molecule. Resolved against document.
state state "idle" | "active" "active" `active` (default) shows full-opacity stroke + travelling pulse; `idle` drops to the container shade and silences the pulse.
directed directed boolean false Render an arrowhead at the target end for directed graphs.

Events

Event Type Description
hp-tether-settle CustomEvent When the tether settles on a vertex pair. detail: { fromEl, toEl, fromVertexIdx, toVertexIdx }

CSS Custom Properties

Property Default Description
--hp-tether-arc-width Stroke width of the arc
--hp-tether-arc-glow Glow filter blur radius
--hp-tether-arc-pulse-dot Diameter of the pulse dot

When to use

Connect distant molecules in node-editor / graph / pipeline surfaces. Arcs route around obstacles automatically (36 candidate vertex pairs, scored by crossings × 1000 + distance). Use directed for flow / causality (data flow, prerequisite trees, signal paths). Skip it for peer relationships where both ends are equivalent. Differentiate states with state="idle|active". Idle dims to secondary-container; active uses the full secondary with the pulse dot travelling along the arc. Tether two atoms that are already axially adjacent. Use <hp-bond> for shared-edge relationships and <hp-tether> for at-distance ones. DOM placement of <hp-tether> is flexible. It tracks endpoints by selector across the entire document, not by DOM ancestry — sibling-of-grid (manual authoring) and child-of-grid (tetherable mode auto-appends here) both work. The element's own position: absolute; inset: 0 covers its positioned ancestor so geometry resolves correctly either way.

Geometry

Each endpoint contributes one of six hex vertices. Control points are pulled along the outward radial direction from each vertex so the curve leaves and enters face-on (Rete editor style). When endpoints move, the tether re-scores all 36 pairs and morphs over ~60ms to the new pick — hysteresis (24px) prevents flapping near the decision boundary.

Performance

Tethers use a single MutationObserver per endpoint, watching the parent grid for q/r and data-hp-dragging changes. Capture-mode transition event listeners catch sibling movements without re-binding. Several hundred tethers on a single surface is fine; thousands are not — for that scale, switch to a canvas / WebGL renderer and apply Hexpunk tokens / classes on the canvas chrome.

No per-component entries in CHANGELOG.md yet.

esc