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

Box-shadow pixel art. Ships built-in morphing icon sets (type), renders custom ASCII art (art), or takes consumer-supplied state records (states); sprite-sheet frames come later. Every state of an icon shares the same logical pixels, so the browser interpolates each pixel's path smoothly.

Examples API Intent Changelog

Built-in sets need no wiring beyond type + interactive. All three idle as the dotted square; hover previews the action, and a click toggles the committed state — click them: menu holds its cross, expandable and dropside flip their chevrons, until clicked again. The committed state rides aria-pressed, which interactive maps to the active frame — the demo's only script is the toggle.

<body> <!-- square · hover: plus · toggled: cross (persists) --> <hp-pixel type="menu" interactive tabindex="0" role="button" aria-pressed="false"></hp-pixel> <!-- square · hover: chevron down · toggled: chevron up --> <hp-pixel type="expandable" interactive tabindex="0" role="button" aria-pressed="false"></hp-pixel> <!-- square · hover: chevron right · toggled: chevron left --> <hp-pixel type="dropside" interactive tabindex="0" role="button" aria-pressed="false"></hp-pixel> <!-- interactive maps aria-pressed="true" to the active frame; your trigger owns the toggling: --> el.addEventListener("click", () => el.setAttribute("aria-pressed", el.getAttribute("aria-pressed") === "true" ? "false" : "true")); </body>

Custom art comes in two flavours: a static ASCII grid, or your own named state record for bespoke morphs — the built-ins use exactly the same mechanism.

<body> <!-- Static ASCII grid: # = lit, . = empty --> <hp-pixel pixel-size="4" id="my-pixel"></hp-pixel> document.getElementById("my-pixel").art = "#.#.#\n#.#.#\n.....\n#.#.#\n#.#.#"; <!-- Bring your own states (same shape the built-ins use) --> <hp-pixel pixel-size="6" id="morph" tabindex="0"></hp-pixel> import { menu } from "@hexpunk/core/pixel-icons/menu"; const el = document.getElementById("morph"); el.states = menu; // explicit states win over type el.interactive = true; // wires :hover / :focus-visible / :active / [aria-pressed] <!-- Multi-colour palette: digits index into it --> el.palette = ["var(--hp-primary)", "var(--hp-secondary)", "var(--hp-warn)"]; el.art = "010\n121\n010"; </body>

Install

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

Properties

Property Attribute Type Default Description
art art string | undefined Static ASCII grid. `#` = lit, `.` = empty, digits index into `palette`.
type type "menu" | "expandable" | "dropside" | undefined Built-in icon set by name — no JS wiring needed. Explicit `.states` / `.palette` win when both are set.
state state string | undefined Current state when `.states` is set. Falls back to `"idle"`.
pixelSize pixel-size number 3 Pixel size in CSS px. Default `3`.
interactive interactive boolean false Auto-swap to `hover` / `focus` / `active` named states on the matching pseudo-classes. No JS state-flip required.

When to use

Reach for a built-in type first — menu for navigation/overflow triggers, expandable for expand/collapse affordances. No JS wiring, and the sets follow the system's state vocabulary. Use the states mode for custom morphs (play ↔ pause, record ↔ stop). Setting interactive wires CSS-only swaps for :hover / :focus-visible / :active / [aria-pressed]; keep every state the same pixel count so the interpolation stays smooth. Drive the morph from an enclosing control instead of the pixel's own hover when the pixel sits inside a larger hitbox: each state is published as a --hp-pixel-<name> custom property, so a parent's selector can set --hp-pixel-shadow: var(--hp-pixel-active). The stats trigger on the hp-background page composes type="expandable" this way. Use art mode for purely decorative pixel marks that don't change — favicons, achievement badges, tiny logos. Set --hp-pixel-shadow inline while interactive is enabled — the inline override beats the CSS-driven swaps and locks the art to one frame. Reach for <hp-pixel> when you need stroke-icon parity. Use <hp-icon> for Lucide-style icons that should match icon-sm/md/lg tokens.
  • 0.1.1-alpha 2026-08-10

    Added

    • <hp-pixel> type attribute — built-in morphing icon sets resolved by name, no JS wiring: menu (dotted square → plus on hover → cross toggled), expandable (square → down-chevron → up-chevron), dropside (square → right-chevron → left-chevron). Faithful to the CodePen reference, including the parked corner pixels that fly outward and fade during arrow morphs (icons can now bundle a palette). Precedence: explicit states > type > art.
esc