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.
Decorative — content
Labelled presentational hex with body-sm typography. Use for readable prose snippets, version markers, info pills.
Decorative — support & slot
Unlabelled framing hexes. support is faint structure (auto aria-hidden); slot is an empty-position marker for permanent drop targets.
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.
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
filled
when the action is destructive / committal and needs maximum emphasis.
body-sm typography for readable prose.
aria-hidden, no focus, no pointer interaction.
content); combined with an interactive variant it
makes a toned button.
filled actions on the same surface — at most one wireframe-
breaking emphasis cell per view.
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. Thefilledboolean 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-pressedswap 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:
/componentsindex 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/componentsas the branch path so the sidebar heading is itself clickable.
Changed
<hp-grid>drag-handle gating. Resolution switched fromevent.target.closest(selector)totarget.querySelector(selector). The closest()-from-event-target approach matched:first-childancestors 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. Withdrag-handle=":first-child"on a cluster whose centre cell sat inside<a><hp-cell></hp-cell></a>, the previousevent.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 ownquerySelectorand asserts that the pointer landed inside the resolved handle.