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

Hex-grid backdrop rendered by WebGL2. Every instance reveals a window onto one shared, page-aligned grid; pointer movement stirs a soft energy wake that brightens the strokes it passes through, then drifts and fades. Pressing on empty space ignites glowing runners that crawl outward along the lattice edges. Falls back to a static CSS tile (with a radial pointer reveal) where WebGL is unavailable or software-rendered.

Cost Measured Context
Script 43 KB min · 14 KB gzip ≈2.5× a typical hexpunk atom (5.6 KB gzip)
Init (once, at connect) ~20 ms WebGL context + shader compile + tile bake; slower on weak GPUs
Animating ~0.1 ms CPU / frame p90 0.7 ms; field sim at 1/8 viewport resolution
Idle 0 frames render loop sleeps; wakes on pointer / scroll / ignite
Examples API Intent Changelog

Move the cursor across the canvas for the wake. Click — or click and hold, then drag — on empty space to ignite runners; the leading pixels burn hotter than the trails they leave.

<body> <div style="position: relative; min-height: 240px"> <hp-background></hp-background> <!-- foreground content here --> </div> </body>

page mode turns the instance into a fixed full-viewport backdrop behind the page content — this site uses one. The canvas never scrolls (the pattern slides beneath it via the shared page alignment), and scrolling itself stirs the field along the edge new content arrives from (scroll-stir).

<body> <main> <hp-background page></hp-background> <!-- page content --> </main> </body>

hp-background as a sibling of the grid cells inside an hp-grid. The grid's drag / pan behaviour is unaffected — the backdrop has pointer-events: none, so cells and pan still receive the pointer.

centre east west north south <body> <hp-grid draggable> <hp-background></hp-background> <hp-cell variant="anchor" q="0" r="0">centre</hp-cell> <hp-cell variant="action" q="1" r="0">east</hp-cell> </hp-grid> </body>
<body> <!-- Denser hexes, longer-lived narrower wake --> <hp-background hex-size="9" decay="0.985" splat-radius="50"></hp-background> <!-- Scroll stirring at the cursor instead of the leading edge --> <hp-background page scroll-stir="pointer"></hp-background> <!-- No scroll response at all --> <hp-background page scroll-stir="off"></hp-background> </body>

Install

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

Properties

Property Attribute Type Default Description
hexSize hex-size number 14 Hex side length in pixels (centre-to-vertex). Smaller = denser pattern. Default 14 — reads as ambient texture, not a focal element.
pointerRadius pointer-radius number 200 Radius in pixels of the pointer reveal on the CSS fallback path (tiers 2/3). The GL path's wake size is governed by splat-radius + diffusion instead. Default 200.
decay decay number 0.964 Energy retention per 60 Hz frame in the GL wake sim — higher values leave longer trails. Clamped to [0.5, 0.995]; wall-clock fade time is roughly proportional to 1/(1 − decay). Default 0.964 ≈ a 1.2–1.6 s visible fade (feel-tuned 2026-08-09 from 0.97 — "1.2× faster").
splatStrength splat-strength number 1 Multiplier on the energy injected per pointer move. Default 1.
splatRadius splat-radius number 80 Gaussian radius of the pointer splat in CSS pixels — the width of the freshly-painted wake before diffusion spreads it. Default 80.
scrollStir scroll-stir "edge" | "pointer" | "band" | "off" "edge" Where scroll velocity stirs the field: "edge" (a band along the viewport edge new content arrives from), "pointer" (at the cursor/touch position), "band" (a soft full-width band at the viewport centre), or "off". Scroll stirring is capped at the bright tier and disabled under reduced motion.

CSS Custom Properties

Property Default Description
--hp-bg-stroke Base stroke colour
--hp-bg-stroke-bright Energy-wake stroke colour
--hp-bg-stroke-hot Ignition-wavefront colour (defaults to the bright colour)
--hp-bg-faint-opacity Base layer opacity (default 0.25)
--hp-bg-bright-opacity Wake layer opacity (default 0.3)
--hp-bg-hot-opacity Ignition-wavefront opacity (default 2× bright, capped at 1)
--hp-bg-pointer-radius Reveal radius on the CSS fallback path (set from pointer-radius)
--hp-bg-decay Overrides the decay attribute
--hp-bg-splat-strength Overrides the splat-strength attribute
--hp-bg-splat-radius Overrides the splat-radius attribute
--hp-bg-z Stacking position in page mode (default -1)

When to use

Use as the backdrop for any surface that benefits from ambient hex texture — demo canvases, dashboards, or (via page) the whole document. The pointer-stirred wake reads as "this surface notices you" without competing with the foreground. Tune hex-size to the surface scale. ~10-14 for small canvases, 18-24 for full-viewport / hero areas. Set the parent container position: relative in contained mode so the backdrop's inset: 0 stretches to fill it. In page mode, mount it directly under a body-level container — an ancestor with a transform, filter, or contain would re-anchor the fixed host. Stack multiple hp-backgrounds in the same stacking context — they render one continuous aligned grid, so overlapping instances double-expose the same strokes. Use as foreground content. The element is aria-hidden and pointer-events: none — it's never the thing the user is meant to interact with.

Interaction model

Three inputs stir the energy field, and each has a strict brightness ceiling. Pointer movement splats energy proportional to speed — capped at the bright tier. Scrolling stirs along the placement chosen by scroll-stir (edge default, pointer, band, off) — also capped at bright. Pressing on a non-interactive target ignites lattice runners, and holding sustains new waves from the live pointer position until release — only the runners' leading pixels reach the hot tier (--hp-bg-stroke-hot). Presses on links, buttons, form controls, or anything focusable never ignite.

Performance

This is one of hexpunk's heavier components — budget for it deliberately. Measured (2026-08-10, lit external): ~43 KB minified / ~14 KB gzip of script (≈2.5× a typical hexpunk atom), ~20 ms one-time init at connect (WebGL context + shader compiles + tile bake; slower on weak hardware), and ~0.1 ms of CPU per frame while animating. It never blocks page paint (it renders nothing until ready, with no layout shift), and at idle it draws zero frames — the render loop runs only while the field holds visible energy.

The hex pattern is baked once into a small tile texture and sampled with wrap — per-frame cost is a texture sample, not per-hex geometry, so density is effectively free. The energy field simulates at 1/8 viewport resolution. A renderer that cannot sustain the sim demotes itself to the static tile. Where WebGL is software-rendered (hardware acceleration off), the element skips the canvas entirely and renders the CSS tile with a radial pointer reveal — set the DevTools console to Verbose to see which path an instance chose (hp-background: rendering via …).

Accessibility

Auto aria-hidden="true" on the host — the element is purely decorative and never announces. Respects prefers-reduced-motion live: the wake, ignition, and scroll stirring are all suppressed and the field clears, leaving the calm static pattern (on the CSS fallback path, the pointer reveal hides the same way).

  • 0.1.1-alpha 2026-08-10

    Added

    • <hp-background> page mode — fixed full-viewport backdrop behind page content (<hp-background page>). The canvas is browser-pinned and exactly viewport-sized; the pattern slides beneath it via the shared page-aligned offset, so it can never be scrolled into a blank region. Adopted by the showcase layout.
    • <hp-background> energy-field wake — pointer movement stirs a persistent low-res field (ping-pong sim at 1/8 viewport resolution: 5-tap diffusion + dt-normalized decay) instead of the old instantaneous halo. Splats are segments from the previous to current pointer position with speed-scaled strength, so fast sweeps carve bright continuous wakes and slow hovers glow gently. The render loop runs only while the field holds visible energy — sleep is computed analytically from the decay rate, and at idle zero frames are drawn. Tuning attributes decay / splat-strength / splat-radius, each overridable via --hp-bg-decay / --hp-bg-splat-strength / --hp-bg-splat-radius.
    • <hp-background> click-ignition runners — pressing a non-interactive target (composed-path check + proximity gate) spawns pixel runners that crawl outward along the actual rendered hex lattice, turning at random and branching only away from the press point. Holding the press sustains new waves from the live pointer position; release lets live runners finish. Runner heads carry a draw-time-only hot glow (--hp-bg-stroke-hot / --hp-bg-hot-opacity, defaulting to the bright colour at doubled opacity) while their trails are capped at the bright tier — the hot tier belongs exclusively to ignition. Suppressed under reduced motion.
    • <hp-background> scroll stirring — scroll velocity feeds the field so the effect exists during the most common interaction on every device. Placement via the scroll-stir attribute: edge (default — a band along the viewport edge new content arrives from), pointer, band, or off. Capped at the bright tier.
    • <hp-background> three-tier degradation — hardware GL renders the full effect; software-rendered GL (SwiftShader / llvmpipe / WARP detected via the renderer string, plus an adaptive frame-interval backstop) routes to the CSS tile; no-GL gets the same tile. The tile is applied as a CSS mask over background-color (theme-reactive with zero JS) across two pseudo-element layers, restoring the v1-style radial pointer reveal on the fallback path. A console.debug render-path log (Verbose level) states which tier each instance chose and why.

    Changed

    • <hp-background> rewritten as a by-concern module folder (src/elements/layout/hp-background/ — geometry, gl, bake, render, field, runners, input, fallback, styles), replacing the 1,100-line monolith and the two-layer-SVG render path with a single WebGL2 canvas (baked hex-tile texture + runtime sample-and-blend). Public API preserved; pointer-radius now governs the fallback path's reveal radius (the GL wake is governed by the splat/decay knobs).

    Fixed

    • <hp-background> full-height rendering with hardware acceleration off — the canvas is never larger than the viewport, never JS-repositioned on scroll, and self-corrects its backing store on every draw, eliminating the software-compositor cutoff bug class. Where the compositor still can't display a WebGL canvas reliably (software-rendered GL), the element routes to the CSS tile instead of fighting it.
    • <hp-background> fallback tile theming — the tile was an SVG background-image stroked with currentColor, which a data-URL SVG always resolves to black; as a mask over background-color it now follows light/dark tokens live.
    • <hp-background> loop hardening — reconnect after a DOM move re-initializes GL (was: permanently frozen canvas); a zero-sized host stops its loop instead of spinning rAF; the sleep window counts accumulated sim time rather than wall clock (was: a tab-hide mid-wake could strand a bright streak on screen); splat coordinates resync on geometry/DPR changes (was: phantom full-strength streak); pointer activity outside an instance's reach no longer wakes its loop; the adaptive demotion backstop samples one contiguous wake against a 60 ms median with outlier discard (was: 30 ms — falsely demoted healthy 30 Hz displays).
  • 0.1.0-beta 2026-05-24

    Added

    • <hp-latex> — render-only LaTeX math primitive. Consumer-registered renderer via HpLatex.setRenderer((latex, "inline" | "block") => string | null); @hexpunk/core has zero math-engine dependency. value attribute carries the LaTeX source (text content fallback captured once on connect). Boolean attributes: block (block/display mode, centred + full-width), background (mounts <hp-background> backdrop inside the element, block mode only), copyable (mounts <hp-copy> source button in the top-right corner, block mode only). Without a renderer, source renders mono-spaced fallback; render-error CustomEvent dispatches on renderer throw. Only light-DOM hp-\* element in the system — required for KaTeX's class-scoped CSS to cascade into the rendered output (file header explains the rationale). Not SSR-friendly via @lit-labs/ssr; consumers wanting pre-rendered math in MDX use Astro's remark-math + rehype-katex directly.
esc