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-unfold-list

Ring-expanding unfold primitive. The source hex stays on the surface; detail children explode outward into the 6-ring of axial neighbours, aligned with the surrounding grid's lattice when nested inside <hp-grid> / <hp-cluster>.

Default is ordered — children land in clockwise spiral order with a matching stagger, mirroring an <ol>'s sense of sequence. The unordered boolean shuffles position + stagger together (stable per-mount, so the random layout doesn't reshuffle when reopened), reading like a <ul>.

Two more primitives are coming as siblings, not modes: <hp-unfold-overlay> (lightbox-style hex popover) and <hp-unfold-page> (View Transitions to a new route).

Examples API Intent Changelog

The source is a grid cell at axial (0, 0). On open, children fill the six axial neighbours clockwise from the top-left, with a matching stagger. All children share the source's hex size so the spiral lattice stays clean — mixed sizes break the step math and overlap.

pipeline ingest validate transform enrich emit log <body> <hp-grid> <hp-unfold-list q="0" r="0"> <hp-cell variant="anchor" slot="source">pipeline</hp-cell> <hp-cell variant="content">stage 1 — ingest</hp-cell> <hp-cell variant="content">stage 2 — transform</hp-cell> <hp-cell variant="content">stage 3 — emit</hp-cell> <hp-cell tone="positive">online</hp-cell> </hp-unfold-list> </hp-grid> const unfold = document.querySelector("hp-unfold-list"); unfold.addEventListener("hp-unfold-open", () => { /* … */ }); unfold.addEventListener("hp-unfold-close", () => { /* … */ }); unfold.toggle(); // flip open / closed unfold.close(); // close if open unfold.open = true; // direct property write also works </body>

Three children, randomly assigned to three of the six ring slots. The shuffle is stable — close + reopen lands them at the same positions.

tags cyberpunk wireframe hex-first <body> <hp-unfold-list unordered> <hp-cell variant="anchor" slot="source">tags</hp-cell> <hp-cell variant="content" size="sm">cyberpunk</hp-cell> <hp-cell variant="content" size="sm">wireframe</hp-cell> <hp-cell variant="content" size="sm">hex-first</hp-cell> </hp-unfold-list> </body>

Install

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

Properties

Property Attribute Type Default Description
open open boolean false Reflected open / closed state. Drives the bloom CSS + the `aria-pressed` mirror on the source.
unordered unordered boolean false When set, children land at a randomly-shuffled subset of the 6 ring positions and stagger in random order (both keyed off the same permutation). Without it, children land in clockwise spiral order and stagger in that same order. Mirrors the ordered / unordered semantics of `<ol>` vs `<ul>`.

Events

Event Type Description
CustomEvent
hp-unfold-open Event When the list opens
hp-unfold-close Event When the list closes

Slots

Slot Description
source The trigger hex (always visible)
(default) Children fanned into the ring when open

When to use

Use the default ordered mode for detail molecules where sequence matters — pipeline stages, step lists, breadcrumbs, anything where "first" through "last" has meaning. Use unordered for tag clouds, related-item ribbons, faceted filters, or any set where the children are peers and order shouldn't be implied. The random stagger reads as "all at once" rather than "in this order". Place inside <hp-grid> so the 6-ring lands on the grid's axial lattice — the unfold inherits the grid's --hp-col-step / --hp-row-step so children sit exactly where neighbouring grid cells would. Use for navigation between routes — that's <hp-unfold-page>. Ring expansion is for in-place detail, not page transitions. Pack more than 6 children into a single unfold-list. Extras stay slotted but hidden in this phase. The future spiral-fill pattern will extend into the second ring (12 more positions) for longer lists. Nest deeper than 3 levels of unfold. Per DESIGN.md, the tether-chain becomes navigationally hostile past unfold-depth-max: 3.

Grid alignment

Inside <hp-grid>, the unfold-list's children land at the axial neighbours of the source's (q, r). The grid's occupancy map doesn't currently know about them, so consumers should keep neighbour cells empty — or accept visual overlap until a later phase wires the hp-unfold-open event to a grid-level dim / occupancy hook.

Drag handle (inside the source)

When mounted in light DOM, <hp-unfold-list> auto-inserts an <hp-module-handle slot="handle"> centred inside the source hex and sets drag-handle="[slot='handle']" on itself. Inside an <hp-grid>, only the handle initiates the grid's drag — the rest of the source's hex polygon stays clickable for toggling open / closed. Bbox corners outside the source's polygon fall through to the grid for pan-passthrough as usual.

To opt out, the consumer can remove the auto-inserted handle and clear the drag-handle attribute after mount, or render the unfold-list outside any grid (the handle is harmless decoration in that context).

Keyboard

The source slot auto-gets tabindex="0" on slotchange (consumers can override explicitly). Enter / Space on the focused source toggles open. Esc closes if open and returns focus to the source.

Reduced motion

Children appear / disappear instantly with no stagger when prefers-reduced-motion: reduce is active. The blue → green hue swap on the source (driven by aria-pressed) remains, since hue change is a state signal rather than decorative motion.

No per-component entries in CHANGELOG.md yet.

esc