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-cluster

Multi-hex group layout. Two modes via the layout attribute. rosette (default) is the canonical 5-hex navigation rosette — a centre cell with four named-slot neighbours. honeycomb accepts N default-slot children and packs them in rings outward from the first child (the centre). Drag handling stays in <hp-grid> via its drag-handle selector — for honeycomb mode use :first-child, for rosette use [slot='centre'].

Examples API Intent Changelog deploy cancel profile edit v2.1.3 <body> <hp-cluster> <hp-cell variant="action" slot="top" tabindex="0">deploy</hp-cell> <hp-cell variant="secondary" slot="middle-left" tabindex="0">cancel</hp-cell> <hp-cell variant="anchor" slot="centre">profile</hp-cell> <hp-cell variant="utility" slot="middle-right" tabindex="0">edit</hp-cell> <hp-cell variant="content" slot="bottom" size="sm">v2.1.3</hp-cell> </hp-cluster> <!-- All five slots are optional. Sizes cascade from the cluster's size attribute via --hp-cell and --hp-hex-stroke. --> <hp-cluster size="md"> <hp-cell variant="anchor" slot="centre">profile</hp-cell> <hp-cell variant="action" slot="top">save</hp-cell> </hp-cluster> </body>

The honeycomb demos below wrap the cluster in <hp-grid draggable> so the centre acts as a drag handle for the whole cluster (via the drag-handle=":first-child" selector hp-grid reads on the cluster). <hp-background> sits as a sibling for the ambient hex backdrop. Both are <hp-grid> features — the cluster itself works without either wrapper; drop the wrapper if you just want a static layout.

forms button checkbox radio select slider toggle <body> <!-- hp-grid + hp-background give the cluster a draggable canvas with the ambient hex backdrop. Both are optional — drop the wrapper if you just want a static layout. --> <hp-grid draggable> <hp-background></hp-background> <hp-cluster layout="honeycomb" q="0" r="0" drag-handle=":first-child"> <!-- First child is the centre — variant="anchor" + filled is the draggable handle for the whole cluster. Solid primary hex, visually distinct from the action-variant outer ring. --> <hp-cell variant="anchor" filled tabindex="0">forms</hp-cell> <!-- Remaining children are navigation links — variant="action" is the "click to navigate" CTA variant. Fill ring 1 clockwise from north (6 max). --> <hp-cell variant="action" tabindex="0">button</hp-cell> <hp-cell variant="action" tabindex="0">checkbox</hp-cell> <hp-cell variant="action" tabindex="0">radio</hp-cell> <hp-cell variant="action" tabindex="0">select</hp-cell> <hp-cell variant="action" tabindex="0">slider</hp-cell> <hp-cell variant="action" tabindex="0">toggle</hp-cell> </hp-cluster> </hp-grid> </body> layout background cluster collapsible demo grid scroll separator sidebar toolbar grid 2 scroll 2 extras <body> <hp-grid draggable> <hp-background></hp-background> <hp-cluster layout="honeycomb" q="0" r="0" drag-handle=":first-child"> <hp-cell variant="anchor" filled tabindex="0">layout</hp-cell> <hp-cell variant="action" tabindex="0">background</hp-cell> <hp-cell variant="action" tabindex="0">cluster</hp-cell> <!-- ... 11 more children. Ring 1 fills first (positions 2-7); ring 2 begins at position 8, clockwise from north. --> </hp-cluster> </hp-grid> </body>

Install

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

Properties

Property Attribute Type Default Description
size size "sm" | "md" | "lg" "sm" Cell size for the cluster — `sm` (default), `md`, or `lg`.
layout layout "rosette" | "honeycomb" "rosette" Layout mode. `rosette` is the canonical 5-hex navigation rosette (named slots, current behaviour). `honeycomb` accepts N default-slot children packed into rings outward from the first child.

Slots

Slot Description
(default) Default slot — N children for honeycomb layout. First child is the centre.
centre Rosette centre hex
top Rosette top hex (north neighbour)
middle-left Rosette west-of-centre
middle-right Rosette east-of-centre
bottom Rosette bottom hex (south neighbour)

When to use

Reach for rosette (default) when you have exactly 4 actions around a single focal point — navigation rails, contextual menus, action wheels. The named slots make intent legible at the markup level. Reach for layout="honeycomb" when the count varies or exceeds 4 outer cells — category indexes, attribute groupings, anything packing N items around a label. The first default-slot child is the centre. Use the same atom type for all outer cells when possible. Mixed atom types (e.g., <hp-cell> + raw <hp-hex>) require careful stroke / fill matching to avoid orphan-ring gaps in the overlap region. DESIGN.md's bonding rules cover this in detail. For drag-snap behaviour, place the cluster inside <hp-grid draggable> with a drag-handle attribute pointing at the centre — the cluster itself stays purely layout. Selectors: drag-handle="[slot='centre']" for rosette, drag-handle=":first-child" for honeycomb. Pack more than 19 children into a honeycomb cluster. v1 caps at ring 2 (1 centre + 6 ring 1 + 12 ring 2 = 19). Beyond that, children stack at the centre. For bigger groups, use multiple clusters or drop to a bare <hp-grid> with hand-placed axial coordinates.

Layout math

Both modes share the same effective-cell math as <hp-grid>col_step = cell − hex-stroke — so adjacent strokes share cleanly. Pixel position from axial coords: x = col_step × (q + r/2), y = row_step × r.

Rosette outer slots sit at (0,-1), (-1,0), (1,0), (0,1). Honeycomb ring 1 fills the same four plus the two unused neighbours (1,-1) and (-1,1); ring 2 fills the 12 axial positions at distance 2, clockwise from north. Children fill sequentially by index — centre first, then ring 1, then ring 2.

  • 0.1.0-beta 2026-05-24

    Added

    • <hp-cluster> layout attribute — rosette (default) / honeycomb modes. rosette preserves the canonical 5-hex cross via named slots (centre / top / middle-left / middle-right / bottom). honeycomb accepts N default-slot children: the first child is the centre (axial 0, 0); remaining children fill outward clockwise from north — ring 1 holds 6 positions, ring 2 holds 12 (capped at 19 hexes total in v1). Drag handling stays in <hp-grid> via drag-handle="..."; canonical handle is :first-child for honeycomb mode, [slot='centre'] for rosette. Auto-sizes the host: 2-ring footprint for rosette, 4-ring for honeycomb. Existing rosette usages render unchanged.
    • <hp-cell variant="anchor" filled> styling. The filled boolean now applies to the anchor variant in addition to action. Idle paints fill + stroke + label in --hp-primary / --hp-on-primary; hover / focus-visible / aria-pressed swap the entire trio to --hp-secondary / --hp-on-secondary, matching the unfilled anchor's hue-swap motion. Used by <hp-cluster layout="honeycomb"> as the cluster centre / drag handle so the focal hex reads at a glance.
    • <hp-grid layout="spiral"> / layout="rows"> — FFD bin-pack with per-cell occupancy and hex-adjacency gap rule, sharing a common algorithm and differing only in scan order. On first render and on explicit .pack() calls, direct children are sorted by mask size (largest first), then each is placed at the first free position the chosen strategy returns. spiral scans outward from the origin in honeycomb rings — the largest cluster anchors (0, 0), smaller ones nest around it with ≥1-hex gaps, producing a tight roughly-square honeycomb. rows scans row-major with a width cap (10 axial cells half-width) — the layout grows as left-to-right rows that wrap downward, ideal for full-page-width surfaces. Children publish their actual filled hexes via data-fill-cells (space-separated "q,r" pairs); composite elements like <hp-cluster> publish these on slotchange. Children without the attr are treated as a single hex. The gap check uses hex-adjacency (the 6 axial-distance-1 neighbours) — not rectangular 3×3 padding — so non-symmetric cluster shapes leave their empty corners available for neighbours to tuck into. No auto-repack on resize / slotchange — only on explicit .pack() call or layout attribute toggle. Strategy modules live at src/elements/layout/hp-grid/layouts/{spiral,rows}.ts with shared primitives in layouts/index.ts; full unit-test coverage in layouts/{spiral,rows,layout}.test.ts (20+ tests covering hex-adjacency rejection, rect-diagonal acceptance, the layout's compactness bound, the components-page 12-cluster snapshot, and the row-wrap behaviour).
    • <hp-cluster> per-cell fill mask — both layout="rosette" and layout="honeycomb" now publish their actual filled hexes as data-fill-cells="q,r q,r …" host attribute (rosette: 5 fixed positions; honeycomb: first N entries of the fill-order table, derived from child count on slotchange). Bounding-box mirror attrs data-axial-q-min / q-max / r-min / r-max retained alongside for cheap consumers that only care about overall footprint dimensions. Consumed by <hp-grid layout="spiral"> / layout="rows"> for the tight bin-pack — empty bbox corners of non-symmetric cluster shapes (e.g. honeycomb with only NE ring-2 hexes filled) stay available for neighbour clusters to tuck into.
    • Showcase: /components index page — full-page-width <hp-grid draggable> filling the main column. Each category renders one <hp-cluster layout="honeycomb" drag-handle=":first-child"> with a centre anchor cell (<hp-cell variant="anchor" filled>) for the category label and one <a>-wrapped action cell per component. Web Awesome-style centred header (title + tagline + filter input), filter input live-filters cells by name substring, clusters whose entire ring filters out auto-hide, and a 300ms debounce kicks off a repack so the layout reflows around just the visible clusters. Sitemap's Components branch gains /components as the branch path so the sidebar heading is itself clickable.
    • Showcase: home page redesign — hero (<h1>Hexpunk</h1> + tagline) + Setup section (copy-paste <head> snippet in <hp-code language="html"> with an <hp-copy> button positioned in the top-right corner; the snippet documents the eventual esm.sh CDN surface: two stylesheet links, an importmap, a module script) + Get Started section (5-hex <hp-cluster layout="rosette">: Install centre + Concepts top + Components middle-left + Palette middle-right + GitHub bottom). Drops the old "Spatial primitives" + "Quick links" prose sections.

    Changed

    • <hp-cluster> host pointer-events. Host is now pointer-events: none; ::slotted(*) re-enables pointer-events: auto. The cluster's rectangular bbox no longer intercepts pointerdowns through its empty corners — pan drags on the surrounding <hp-grid> reach the host correctly, and at the page bottom-right the cluster bbox no longer overlaps hp-grid's zoom / recenter controls. Only the actual hex cells catch clicks; the drag-handle child is still interactive because the ::slotted rule re-enables pointer-events on every positioned hex.
esc