Skip to content

Repository files navigation

Halftone UI — every surface printed, not painted

A component library that ships its own printing press.
Buttons, charts, switches and washes are halftone screens pressed onto canvas,
the way ink actually lands on paper.

halftone-ui.com · ▶ Docs & live demos

GitHub stars Zero dependencies No build step License: MIT

A pressed line chart — solid ink above, live grain falling away beneath; the grain flickers because every dot keeps re-testing its threshold


What this is

Most UI libraries paint: a fill is a hex value, a gradient is a CSS function, a chart is an SVG path. Halftone UI prints. Every fill is a live canvas holding a seeded dot cloud, and each dot carries its own threshold. A component supplies a tone function — how dark is the ink at this point? — and the press keeps the dots that tone can reach.

That single operation — threshold a tone field against a screen — is what halftone means. It's also the entire library.

// every component is just a tone function 0..1
const meter = surface(canvas, {
  tone: (p, W, H) => (p.x / W < 0.72 ? 0.95 : 0.05),
  pattern: "hatch",
})

// the press: keep the dots whose threshold the local tone can reach
for (const p of dots) {
  if (tone(p) > p.threshold) ink(p)
}

The dots come from a seeded Poisson-disk cloud — blue noise, which is exactly what stochastic screening uses on a real press. Because the seed is deterministic, a reload gives you the same ten thousand dots; reroll and the whole page reprints at once. Animation never tweens CSS: a value glides, every dot re-tests its threshold, and the grain itself is the motion.

It's a loving riff on dither-ui — same docs-site format, different printmaking tradition.

Quick start

No install. It's one HTML file.

git clone https://github.com/ecgang/halftone-ui.git
open halftone-ui/dist/index.html

Or just download dist/index.html and double-click it. Everything — engine, docs, demos, themes — is inside, in one self-contained file. The docs are dogfooded on the real library: dist/index.html is the framework-free core from halftone-kit/core/ inlined by tools/build-standalone.mjs.

Tip

Try the topbar: toggles the designed light mode, opens the OKLCH wheel (drag the ring — the whole site rethemes), opens the global grain dials, and reroll reprints every surface from a new seed.

Install

There's no npm package, by design. Halftone UI ships as source you copy into your own repo — shadcn/dither-ui style — so you own the code instead of pinning a version.

# framework-free core (zero deps)
npx degit ecgang/halftone-ui/halftone-kit/core your-app/src/halftone/core

# + React adapter (needs core beside it — react/ imports '../core')
npx degit ecgang/halftone-ui/halftone-kit/react your-app/src/halftone/react
npx degit ecgang/halftone-ui/halftone-kit/core  your-app/src/halftone/core

# + Vue adapter (same requirement — vue/ imports '../core' too)
npx degit ecgang/halftone-ui/halftone-kit/vue your-app/src/halftone/vue
npx degit ecgang/halftone-ui/halftone-kit/core your-app/src/halftone/core

react/ and vue/ are thin adapters over core/ — they always import it as ../core, so it has to live one directory up from wherever you drop the adapter folder.

Vanilla

import { createPressContext, press } from './halftone/core/index.js'

const ctx = createPressContext()
press(document.querySelector('canvas'), {
  field: (u, v) => (v > 0.6 ? 1 : 0), // tone 0..1; u,v normalized, v=0 top
}, ctx)

React

import { HalftoneProvider, Surface } from './halftone/react/index.js'

export default function App() {
  return (
    <HalftoneProvider>
      <Surface field={(u, v) => (v > 0.6 ? 1 : 0)} style={{ height: 160 }} />
    </HalftoneProvider>
  )
}

The adapter is .jsx — your own bundler (Vite, webpack, Next) compiles it like any other component in your app.

Vue

import { h } from 'vue'
import { HalftoneProvider, Surface } from './halftone/vue/index.js'

export default {
  render() {
    return h(HalftoneProvider, () =>
      h(Surface, { field: (u, v) => (v > 0.6 ? 1 : 0), style: 'height:160px' }),
    )
  },
}

Plain .js render functions (h()) — no SFC, nothing to compile, drop it straight into a Vite/Nuxt build.

Components

Component Wraps (real DOM / a11y)
Surface the base pressed canvas — bring your own semantic wrapper
Text pair with your own visually-hidden heading
Image pair with your own visually-hidden <img alt>
Button a real <button>
Meter a real <progress>
Card a real container element (div by default, as to change it)
BarChart a real <table> with <caption>
LineChart a real <table> with <caption>

Every canvas is aria-hidden decoration — the table above is where the actual semantics live.

Full API + prop reference: halftone-kit/README.md. Play with every screen and dial live in Studio.

Features

  • 88 documented sections — primitives (switch, slider, OTP field, dialogs, menus, combobox), charts (line, pie, radar, area, bars, heatmap, donut, sparkline), and whole page examples (dashboard, pricing, billing, sign-in flows)
  • Four halftone screenshatch (crosshatch, the default), stipple (stochastic/FM), lines (line screen), waves. Every pressed example carries a picker: re-press it live. Swap the screen and the whole page changes character — the tone field underneath never moves
  • A real four-plate press — CMYK separation at the true process screen angles (C 15°, M 75°, Y 0°, K 45°), with plate order and adjustable misregistration
  • Global grain dials — one control rescales the screen, the ink weight and the washes across every surface on the page at once
  • Real light & dark themes — a designed print-shop-cream light mode and an archival-black dark mode, not an inversion filter
  • OKLCH hue wheel — drag a ring in the topbar and every pigment rotates through OKLCH hue space live; neutrals stay neutral
  • Image halftoning — any raster becomes tone: luminance drives the dot field, so photos re-print in the current ink
  • Smudge 😈 — the resident ink imp, pressed from the same engine as everything else
  • Zero dependencies, one file — no build step, no CDN, works from file://
  • Accessible — native elements underneath (<dialog>, <details>, real inputs), prefers-reduced-motion respected

Gallery

Pie chart in light mode — four plates stacked on one ring A photo re-printed as halftone by the Image primitive
Smudge, the Halftone UI ink imp Line chart in dark mode

Credits

  • Format and spirit: dither-ui
  • Crosshatch inspiration: Texturelabs' Retratone halftone technique
  • Engine, docs, and Smudge: built with Claude Code

License

MIT © Eric Gang

About

A component library that ships its own printing press. Every surface is printed, not painted — halftone screens pressed onto canvas. One file, zero dependencies.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages