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

LaTeX math primitive. Render-only Lit element with a consumer-registered engine — hp-latex ships no math engine. A consumer that wants math installs the engine of their choice (KaTeX, MathJax, Temml, …) and registers an adapter once at startup via HpLatex.setRenderer(...). Source comes from the value attribute or the element's text content (attribute wins). Without a registered renderer, the source renders as mono-spaced text — same fallback shape as <hp-code> with no tokeniser.

This showcase has KaTeX registered globally with throwOnError: false — every <hp-latex> on the page renders via KaTeX, and broken LaTeX emits an inline red .katex-error span instead of throwing. hp-latex is the only light-DOM hp-* element in the system — see the Intent tab for the rationale. Examples API Intent Changelog

Einstein's mass-energy equivalence is — one of the cleanest statements in physics. The Pythagorean identity sits in the same shape. Greek letters work too: .

<body> <p> Mass-energy equivalence is <hp-latex value="E = mc^2"></hp-latex> — one of the cleanest statements in physics. Greek letters work too: <hp-latex value="\alpha + \beta = \gamma"></hp-latex>. </p> </body>
<body> <hp-latex block background value="\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}"></hp-latex> <hp-latex block background value="\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}"></hp-latex> <hp-latex block background value="\lim_{x \to 0} \frac{\sin x}{x} = 1"></hp-latex> </body> <body> <hp-latex block background value="A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}" ></hp-latex> </body>

The roots of are given by:

<body> <p> The roots of <hp-latex value="ax^2 + bx + c = 0"></hp-latex> are given by: </p> <hp-latex block background value="x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}" ></hp-latex> </body>
<body> <hp-latex block background value="\begin{aligned} e^{i\pi} + 1 &= 0 \\ e^{i\pi} &= -1 \\ \ln(-1) &= i\pi \end{aligned}" ></hp-latex> </body> <body> <hp-latex block background value="f(x) = \begin{cases} x^2 & \text{if } x \geq 0 \\ -x & \text{if } x < 0 \end{cases}" ></hp-latex> </body> <body> <!-- Omit the `background` attribute for naked block math — useful when the surrounding surface already has its own chrome. --> <hp-latex block value="\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}"></hp-latex> </body> <body> <hp-latex block background copyable value="e^{i\pi} + 1 = 0" ></hp-latex> </body>

Good:

Bad:

<body> <p>Good: <hp-latex value="\sqrt{x + 1}"></hp-latex></p> <p>Bad: <hp-latex value="\notarealcommand{x}"></hp-latex></p> </body>

Install

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

Properties

Property Attribute Type Default Description
value value string "" LaTeX source string
block block boolean false Switch to block (display) mode (default: inline)
background background boolean false Mount an hp-background backdrop (block-mode only)
copyable copyable boolean false Show a copy-source button in the top-right corner (block-mode only)

Events

Event Type Description
render-error CustomEvent Bubbling CustomEvent when the renderer throws; detail = { error, latex }

Setting up the renderer

hp-latex ships no engine to keep @hexpunk/core dependency-free. Install KaTeX (or another engine), register it once at startup. Every connected <hp-latex> re-renders automatically when a new renderer is set.

bun add katex import "@hexpunk/core/hp-latex.css"; import "katex/dist/katex.min.css"; import katex from "katex"; import { HpLatex } from "@hexpunk/core"; HpLatex.setRenderer((source, mode) => { try { return katex.renderToString(source, { displayMode: mode === "block", throwOnError: false, }); } catch { return null; } });

throwOnError: false tells KaTeX to render an inline red error span instead of throwing on malformed input — the rest of the page keeps rendering. The try/catch returns null on any unexpected failure, which triggers hp-latex's mono-spaced source fallback. Failures also dispatch a bubbling render-error CustomEvent (detail: { error, latex }) so consumers can log them.

Why light DOM?

hp-latex is the only light-DOM hp-* element in the system — createRenderRoot() returns this. KaTeX outputs HTML scoped by .katex / .katex-mathml / .katex-html classes and depends on ~2000 lines of CSS plus @font-face rules to render correctly; shadow-DOM encapsulation would block that cascade. Light DOM lets the consumer's global katex.min.css reach the math naturally. The deviation from the hp-* convention is intentional and documented in the element's source.

Alternatives

The renderer slot is engine-agnostic — anything that returns an HTML string works:

  • KaTeX (0.17.0, May 2026). Fast, synchronous, comprehensive LaTeX subset. ~70 KB JS + ~25 KB CSS + font files. Default pick.
  • Temml (0.13.3, May 2026). Pure LaTeX → MathML converter. ~30 KB, no fonts (relies on browser MathML). Smaller payload; rendering fidelity varies by browser.
  • MathJax v4 (4.1.2, May 2026). Broader LaTeX coverage, accessibility extensions, async renderer. ~400 KB+. Reach for it when KaTeX's subset isn't enough.

For build-time pre-rendered math in static MDX docs (independent of hp-latex), use Astro's remark-math + rehype-katex plugins. That path produces zero-runtime-JS math directly in HTML and is the right call for static docs. hp-latex is for dynamic / interactive / programmatically-driven math.

When to use

Use for any math expression a consumer should see rendered — inline references, block formulas, classroom-style derivations, live previews of user-entered LaTeX. Set the value attribute when the source contains characters HTML doesn't love (<, >, & inside inequalities). The slot fallback is fine for static prose where the source is plain. Pair block with background for a one-off "system-styled" featured equation — drops an <hp-background> behind the math without needing a wrapper. Add copyable when readers might want to grab the LaTeX source — docs pages walking through derivations, reference cards, anywhere the expression itself is the takeaway. The button writes the value attribute to the clipboard via the async Clipboard API. Stuff LaTeX into <hp-code> when you want it rendered. hp-code treats its content as plain code (or syntax-highlighted), not math. Reach for hp-latex when the intent is "show the math". Use hp-latex on pages built with Astro's output: "static" if you need pre-rendered math at build time — light DOM is incompatible with @lit-labs/ssr. Use remark-math + rehype-katex in your Astro/MDX config instead.

Accessibility

KaTeX emits MathML alongside its rendered HTML so screen readers can announce the math semantically (<math> inside the .katex-mathml span). The visual .katex-html span is aria-hidden. No extra setup needed.

For the no-renderer fallback, the source renders as a plain <span class="hp-latex-source"> — screen readers read the LaTeX source as text. Consumers wanting better fallback semantics can listen for render-error and surface a user-facing message.

  • 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.
    • <hp-copy> — copy-to-clipboard button widget. Lucide copy icon + slotted label (translatable per-instance, default "Copy") + sibling "Copied" toast with aria-live="polite". value attribute for the text to copy; copied attribute for the toast text; icon-only boolean drops the visible label while keeping it in the a11y tree. Bubbling hp-copy-success / hp-copy-error events. Async Clipboard API only — no execCommand fallback. Now powers <hp-demo>'s copy-code action and <hp-latex>'s source-copy button.
    • @hexpunk/core/hp-latex.css — light-DOM chrome stylesheet for <hp-latex> (display modes, backdrop positioning, source-fallback typography). Consumer imports alongside elements.css and katex.min.css.

    Changed

    • <hp-demo> copy action — replaced the bespoke inline copy button + animated toast with <hp-copy> so the same widget powers both <hp-demo> and <hp-latex>. Slot-text source caching via slotchange on the code slot; no behaviour change for consumers.
esc