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

The universal labelled hex. Subsumes anchor / action / secondary / utility (interactive), content / support / slot (decorative), and positive / warn / alert / error (tone overlay) into one element. Picks behaviour via variant; layers semantic colour via tone; flips to filled emphasis with active. Focus indicator is the rotating-trace animation, built in — no separate hp-trace element needed.

Examples API Intent Changelog profile deploy archive tab me active <body> <hp-cell variant="anchor">profile</hp-cell> <hp-cell variant="anchor" size="md">deploy</hp-cell> <hp-cell variant="anchor" size="lg">archive</hp-cell> <hp-cell variant="anchor" tabindex="0">tab me</hp-cell> <hp-cell variant="anchor" aria-pressed="true">active</hp-cell> </body> profile layout forms <body> <!-- Filled-anchor reads as a solid primary hex, distinct from the unfilled anchor neighbours in a cluster. Hover / focus swaps the whole fill+stroke+label to secondary (matches the unfilled anchor hue-swap motion, on a filled surface). Pairs with <hp-cluster layout="honeycomb"> as the cluster centre. --> <hp-cell variant="anchor" filled tabindex="0">profile</hp-cell> <hp-cell variant="anchor" filled size="md" tabindex="0">layout</hp-cell> <hp-cell variant="anchor" filled size="lg" tabindex="0">forms</hp-cell> </body> deploy launch go live commit <body> <hp-cell variant="action" tabindex="0">deploy</hp-cell> <hp-cell variant="action" filled tabindex="0">go live</hp-cell> </body> cancel undo edit close filter <body> <hp-cell variant="secondary" tabindex="0">cancel</hp-cell> <hp-cell variant="utility" tabindex="0">edit</hp-cell> </body>

Decorative — content

Labelled presentational hex with body-sm typography. Use for readable prose snippets, version markers, info pills.

v2.1.3 2 of 18 quests build #4421 · all green · 24s <body> <hp-cell variant="content" size="sm">v2.1.3</hp-cell> <hp-cell variant="content">2 of 18 quests</hp-cell> <hp-cell variant="content" size="lg">build #4421 · all green · 24s</hp-cell> </body>

Decorative — support & slot

Unlabelled framing hexes. support is faint structure (auto aria-hidden); slot is an empty-position marker for permanent drop targets.

<body> <hp-cell variant="support"></hp-cell> <hp-cell variant="slot"></hp-cell> </body>

Tone — semantic indicator

Sets variant="content" automatically when no variant is given. Hollow at rest with the tone stroke; pair with active for the filled tone-container fill.

online pending restricted failed <body> <hp-cell tone="positive">online</hp-cell> <hp-cell tone="warn">pending</hp-cell> <hp-cell tone="alert">restricted</hp-cell> <hp-cell tone="error">failed</hp-cell> </body> online pending restricted failed <body> <hp-cell tone="positive" active>online</hp-cell> <hp-cell tone="warn" active>pending</hp-cell> <hp-cell tone="alert" active>restricted</hp-cell> <hp-cell tone="error" active>failed</hp-cell> </body>

Install

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

Properties

Property Attribute Type Default Description
variant variant HpCellVariant "anchor" Role + visual preset. Interactive variants (`anchor|action|secondary|utility`) lift on hover, show the focus trace, and accept keyboard input via the host's `tabindex`. Decorative variants (`content|support|slot`) are static.
size size "xxs" | "xs" | "sm" | "md" | "lg" "sm" Cell size. - `xxs` (20px) — dense inline form-control hex - `xs` (32px) — comfortable inline form-control hex - `sm` (100px, default) — content-hex / general UI tile - `md` (180px, flat-top) — large content tile - `lg` (320px) — feature tile Variant-aware auto-defaults apply when `size` isn't set: `content` → `md`, `support` → `lg`. Form-input components (hp-checkbox, hp-radio) own their own xs/xxs defaults via their own `size` attribute.
tone tone HpCellTone "neutral" Semantic tone overlay. `neutral` (default) leaves variant tokens untouched; the others swap stroke / label to the matching semantic colour pair.
active active boolean false When set with a non-neutral tone, fills the hex with the tone-container colour and flips the label to the on-container contrast pair. The cell reads as "this state is in effect".
filled filled boolean false Filled-hex paint. Works on `variant="action"` (high-emphasis CTA; darkens to primary-container on hover/focus) and `variant="anchor"` (identity / grip / drag-handle; hue-swaps to secondary on hover/focus/aria-pressed — same as the unfilled anchor engagement motion). Both paint the polygon with `--hp-primary` and flip the label to `--hp-on-primary` at rest.

Slots

Slot Description
(default) Label content of the cell (text or inline icon)

CSS Custom Properties

Property Default Description
--hp-stroke-color Outline colour of the hex polygon
--hp-cell-label-color Label text colour
--hp-hex-fill Inner fill (defaults to canvas, set for swatches)
--hp-cursor Cursor override (e.g., grab inside hp-grid drags)
--hp-hex-pointer-events Pointer-events on the inner polygons

CSS Parts

Part Description
cell The wrapping cell element (positioned ancestor for label / trace)
label The label container (when the variant has a label)

When to use

anchor for identity / section markers / drag handles. The hollow blue stroke reads as a stable identifier, not a call to action. action for the one primary CTA on a surface. Add filled when the action is destructive / committal and needs maximum emphasis. secondary for the alternate path next to an action — typically Cancel, Back, Skip. utility for short adjacent actions (edit / close / filter / recover) where 2-4 character labels or icons fit naturally. content for labelled presentational hexes (version markers, info pills, stage labels in pipelines). Uses body-sm typography for readable prose. support for faint framing hexes that decorate a surface without carrying meaning. Auto aria-hidden, no focus, no pointer interaction. slot for empty-position markers — permanent drop targets in dashboards, game HUD inventories, widget areas. tone for any semantic colour overlay (positive / warn / alert / error). Layers on top of any variant. By itself it implies a decorative status indicator (variant defaults to content); combined with an interactive variant it makes a toned button. Stack two filled actions on the same surface — at most one wireframe- breaking emphasis cell per view. Use anchor as a button. The label colour stays on-surface at rest; readers won't recognise it as actionable.

Focus indicator

Every interactive cell renders the three trapezoidal trace edges in its shadow root, animating in via a staggered grow-in transition on :focus-visible and unwinding in reverse on blur. Stagger driven by --hp-unfold-stagger; respects prefers-reduced-motion.

Accessibility

<hp-cell> is non-focusable by default. The consumer (or a parent <hp-module>) sets tabindex="0" when keyboard focus is required. For toggle / pressed semantics, set aria-pressed="true|false" — the system reads this attribute for state styling AND screen readers announce it correctly. Decorative variants (support, slot) auto-stamp aria-hidden="true".

Composition

Slot into <hp-cluster> for the 5-hex rosette pattern, or into <hp-grid> with q/r attributes for free axial placement. Inside both, --hp-cursor overrides to grab / grabbing during drag — no extra wiring.

  • 0.1.0-beta 2026-05-24

    Added

    • <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.
    • 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.

    Changed

    • <hp-grid> drag-handle gating. Resolution switched from event.target.closest(selector) to target.querySelector(selector). The closest()-from-event-target approach matched :first-child ancestors transitively — e.g. inside <a><hp-cell></hp-cell></a>, the hp-cell is its <a> parent's first child, so closest() returned it and the drag-handle gate passed even when the click was outside the cluster's actual centre handle. querySelector resolves to the cluster's first matching descendant in DOM order (the actual centre), and the gate checks whether the pointerdown landed on it or one of its descendants.

    Fixed

    • <hp-grid> drag-handle resolution false-positive inside anchor wrappers. With drag-handle=":first-child" on a cluster whose centre cell sat inside <a><hp-cell></hp-cell></a>, the previous event.target.closest(":first-child") walk matched the wrapped hp-cell (it's its parent's first child), so clicking an outer link incorrectly initiated a cluster drag instead of navigating. Resolution now uses the cluster's own querySelector and asserts that the pointer landed inside the resolved handle.
esc