halftone·ui docs
seed 1859
github studio →

Halftone UI

A component library that ships its own printing press. This wordmark is not a font rendering — it is four ink plates at the true process screen angles, each dot sized by the tone underneath it. Every surface below is pressed by the same engine.

seed 1859

Quick start

A halftone UI toolkit — charts, primitives and app components on one seed-generative point-cloud engine. The engine is plain TypeScript with no framework in it; @halftone-ui/vue and @halftone-ui/react are thin adapters over the same core.

Memory68%
# npm i @halftone-ui/vue

<script setup>
import { HButton, HMeter } from "@halftone-ui/vue"
</script>

<template>
  <HButton color="purple">Create account</HButton>
  <HMeter label="Memory" :value="68" />
</template>

Every visual surface is a canvas driven by tone(x, y, t) → 0..1 evaluated over a seeded blue-noise point cloud — a point is inked when tone beats its threshold. Components are the real DOM (buttons, inputs, tablists); the halftone is a decorative layer painted behind them, so semantics and accessibility come for free.

Styling

Seven palette colors — green blue purple pink orange red grey — plus ink and surface tokens. Pass color to any component; density and dot radius are theme tokens shared by every surface.

Seeds

Every point cloud derives from one integer seed. Reroll it (topbar, or provider.reroll()) and every surface on the page regenerates its identity — same data, new grain. Components take a seedOffset so neighbors never share a cloud.

Accessibility

Canvases are aria-hidden decoration; interaction lives on native elements underneath (real <button>s, roving-tabindex tablists, aria-pressed toggles). Values are always rendered as text, never only as density. prefers-reduced-motion disables drift, blooms and entrance animation.

Composition

Components are plain markup wrapping a canvas. Parts render in template order, and template order is paint order — whatever is pressed last sits on top. No portals, no z-index arms race.

workspace

stablev0.1.0
Usage64%
<HCard>
  <HBadge color="green">stable</HBadge>
  <HMeter label="Usage" :value="64" />
  <HButton color="purple" block>Deploy</HButton>
</HCard>

Motion

Surfaces never tween CSS — they re-press. State glides a value, every dot re-tests its threshold against it, and the grain itself is the animation. Honors prefers-reduced-motion by jumping straight to the final frame.

<HWash color="pink" :reveal="revealed" />
<HButton color="pink" @click="replay">Replay</HButton>
PropTypeDefault
revealnumber 0..11
glidenumber (easing k)0.16

Examples

Sign up

A signup card with a working plan picker — segmented control from plain aria-pressed buttons, purple wash behind.

<HCard wash="purple">
  <HInput v-model="name" placeholder="Ada Byte" />
  <HSegmented v-model="plan" :options="['Free','Pro','Scale']" />
  <HButton color="purple" block>Create account</HButton>
</HCard>

Monitoring

A static gauge on the progress track — the fill picks its own color: green below the low mark, orange between, red above the high mark.

Disk35%
Memory68%
CPU92%
<HMeter label="Disk"   :value="35" />
<HMeter label="Memory" :value="68" />
<HMeter label="CPU"    :value="92" />

Dashboard

Stat tiles, sparklines, and capacity gauges — the whole overview page pressed from one seed.

4,812
requests/min
99.98%
uptime, 30d
241ms
p95 latency

capacity

Disk41%
Memory72%
<HStat label="requests/min" value="4,812" :trend="reqs" />
<HStat label="uptime, 30d" value="99.98%" :trend="uptime" />
<HMeter label="Disk" :value="41" />

App shell

Sidebar, topbar, content well. The shell is chrome-only — every surface inside stays a canvas you already know.

⬢ halftone Overview Deploys Logs Settings
productionhealthy
18
deploys this week
3
open incidents
<HShell>
  <template #nav><HNav :items="nav" /></template>
  <router-view />
</HShell>

Team

A members table where state rides on badges, not colored text.

MemberRoleStatusLast active
Ada ByteOwneronlinenow
Grace PointMaintaineronline2m ago
Alan Turing-TestMemberaway1h ago
Linus KernelReviewerbusy3h ago
<HTable :rows="members">
  <template #status="{ row }">
    <HBadge :color="row.color">{{ row.status }}</HBadge>
  </template>
</HTable>

Usage & billing

Metered usage against plan limits, and the invoices to prove it.

this cycle · Pro plan

API calls6.2M / 10M
Seats10 / 12
InvoiceAmountStatus
June 2026$49.00paid
May 2026$49.00paid
April 2026$42.50paid
<HMeter label="API calls" :value="62" caption="6.2M / 10M" />
<HTable :rows="invoices" />

Sign in

The return trip: same card as sign up, quieter ask, blue wash.

<HCard wash="blue">
  <HInput v-model="email" type="email" />
  <HInput v-model="password" type="password" />
  <HButton color="blue" block>Sign in</HButton>
</HCard>

Two-factor

Six boxes, auto-advance, backspace walks backwards. The OTP field from the primitives, dressed for the door.

<HCard wash="orange">
  <HOtpField v-model="code" :length="6" />
  <HButton color="orange" block>Verify</HButton>
</HCard>

Pricing

Three cards; the accent border does the selling, the grain does the texture.

Free

$0 /mo
  • 1 project
  • Community seeds
  • MIT license

Pro

$49 /mo
  • Unlimited projects
  • Ink styles
  • Priority press

Scale

$249 /mo
  • SSO + audit log
  • Custom plates
  • Support SLA
<HPricingCard v-for="plan in plans" :key="plan.name"
  :plan="plan" :featured="plan.name === 'Pro'" />

Activity feed

A pressed dot per event — color is the event type, time keeps to the margin.

Ada deployed v0.4.2 to production
Grace opened a pull request
p95 latency crossed 400ms, then recovered
Alan rotated the API keys
Build #812 failed on main
<HFeed :items="events">
  <template #item="{ event }">
    <HDot :color="event.color" /> {{ event.text }}
  </template>
</HFeed>

Changelog

Versions as badges, changes as sentences. Herald would approve.

v0.4.0

Ink styles ship: four-plate crosshatch on any component via the inks prop.

New OKLCH theme wheel — rotate every pigment at once.

v0.3.0

Charts: line, pie, and radar join area and bars.

v0.2.0

Seeded Poisson clouds became deterministic across SSR and client.

<HChangelog :releases="releases" />

Charts

Area Chart

Children-as-config, like the reference kit — compose parts inside a sized root. Hover for the crosshair and tooltip; click a legend entry to isolate a series.

data table
<AreaChart :data="data" :config="config">
  <Grid />
  <XAxis dataKey="month" />
  <YAxis />
  <Area dataKey="desktop" />
  <Area dataKey="mobile" />
  <Legend />
  <Tooltip labelKey="month" />
</AreaChart>

Bar Chart

Tone thresholding against the same point cloud; hover a bar for bloom and its value.

<BarChart :data="deploys" :config="config">
  <XAxis dataKey="week" />
  <Bar dataKey="count" is-clickable />
</BarChart>

Sparkline

A stat tile with a halftone trace — number first, grain second.

4,812
requests/min
218ms
p95 latency
0.02%
error rate
<Sparkline :data="reqs" color="blue" />

Line Chart

One series, one stroke — the line is solid ink and a comet of grain falls away beneath it.

<HLineChart :data="requests" color="blue" />
PropTypeDefault
datanumber[]
colorHalftoneColor"blue"
falloffnumber0.5

Pie Chart

Four plates stacked on one ring — each slice is its own canvas, so each keeps its own ink.

1.2Mevents
api 42%web 25%cron 19%other 14%
<HPieChart :slices="[42, 25, 19, 14]"
  :colors="['blue', 'orange', 'green', 'grey']" />

Radar

A pentagon web drawn in hairline, filled with grain that thickens toward the rim.

speed · a11y · dx · size · ssr
<HRadarChart :axes="['speed', 'a11y', 'dx', 'size', 'ssr']"
  :values="[84, 58, 74, 46, 68]" color="purple" />

Primitives

Button

Halftone fill on a native button; hover blooms the grain. Outline variant keeps a sparse wash.

<HButton color="green">Save</HButton>
<HButton color="red">Export</HButton>
<HButton color="grey" variant="outline">Cancel</HButton>
PropTypeDefault
colorHalftoneColor"grey"
variant"fill" | "outline""fill"
blockbooleanfalse

Badge

A quiet wash chip in any palette color.

stable v0.1.0 beta breaking seed 1859
<HBadge color="green">stable</HBadge>

Tabs

A roving-tabindex tablist with a pressed underline that slides to the active tab. Arrow keys move selection.

<HTabs v-model="tab" :tabs="['Overview','Metrics','Logs']" />

Variants

segmented
washed

Vertical · badges · disabled

Inbox — 12 items.
PropTypeDefault
tabs(string | { value, label?, badge?, disabled? })[]
modelValuestring
variant"underline" | "segmented" | "washed""underline"
verticalbooleanfalse

Meter

The gauge from the Monitoring example, on its own. Color resolves automatically from the low/high marks unless pinned.

Build cache54%
<HMeter label="Build cache" :value="54" :low="40" :high="80" />
PropTypeDefault
valuenumber
lownumber40
highnumber80
colorHalftoneColorauto

Toast

Imperative, like every toast — but the wash behind it is the same engine.

import { toast } from "@halftone-ui/vue"

toast("Deploy finished", { color: "blue", duration: 3500 })
PropTypeDefault
messagestring
opts.colorHalftoneColor"blue"
opts.durationnumber3500

Avatar

Seed-generative identicons — symmetric noise, screened. Click one to reroll it.

<HAvatar :seed="user.id" />

Skeleton

Loading placeholders whose shimmer is a density wave traveling through the grain.

<HSkeleton :lines="3" />

Gradient

A wash — seeded value-noise drifting in time. Use it behind cards and heroes.

<HGradient color="purple" drift />

Image

Any raster becomes tone: luminance drives the threshold field, so the photo is re-printed in dots of the current ink. Retheme and it re-inks itself.

eric.jpg, pressed at 1.9px grain
<HImage src="/eric.jpg" :grain="1.9" />
PropTypeDefault
srcstring
grainnumber (px)2.2
gammanumber1.3

Switch

The track floods with grain as it switches on; the thumb is plain CSS riding above the press.

<HSwitch v-model="notify" color="green" />
PropTypeDefault
modelValuebooleanfalse
colorHalftoneColor"green"

Checkbox

The box fills left-to-right like a pen stroke; the tick is knocked out in paper color.

<HCheckbox v-model="email">Email me on deploy</HCheckbox>

Checkbox Group

Independent checks that report as one array.

<HCheckboxGroup v-model="targets">
  <HCheckbox value="vue">Vue</HCheckbox>
  <HCheckbox value="react">React</HCheckbox>
  <HCheckbox value="svelte">Svelte</HCheckbox>
</HCheckboxGroup>

Radio

One dot of ink at a time.

<HRadioGroup v-model="density"
  :options="['Comfortable', 'Compact', 'Dense']" />

Toggle

A pressed-state button: on means inked.

<HToggle v-model="fav">★ Favorite</HToggle>

Toggle Group

Exclusive pressure — one plate down at a time.

<HToggleGroup v-model="align"
  :options="['Left', 'Center', 'Right']" />

Slider

A native range input over a halftone fill — grain pours in behind the thumb.

64
<HSlider v-model="volume" color="blue" />
PropTypeDefault
modelValuenumber0
min / max / stepnumber0 / 100 / 1
colorHalftoneColor"blue"

Progress

Determinate takes a value; indeterminate breathes on a sine and never lies about being done.

<HProgress indeterminate color="purple" />
PropTypeDefault
valuenumber | undefinedundefined
indeterminatebooleanfalse

Spinner

A comet of ink chasing its tail. Density falls off behind the head, like a nib running dry.

<HSpinner color="blue" />

Input

A plain input on card paper. Focus swaps the border to the accent — no glow, no ring, just ink.

<HInput v-model="email" label="Email" type="email" />

Textarea

Same paper, more room.

<HTextarea v-model="notes" label="Release notes" :rows="4" />

Field

Label, control, and hint stacked with one rhythm. Errors swap the hint, not the layout.

Shown on your public profile.
<HField label="Display name" hint="Shown on your public profile.">
  <HInput v-model="name" />
</HField>

Fieldset

A drawn border and a legend — the oldest grouping primitive on the web, kept.

Notifications
<HFieldset legend="Notifications">
  <HCheckbox v-model="deploys">Deploy succeeded</HCheckbox>
  <HCheckbox v-model="digest">Weekly digest</HCheckbox>
</HFieldset>

Form

Fields compose into a form; submit is just a button that means it.

<HForm @submit="create">
  <HInput v-model="project" label="Project" />
  <HSelect v-model="region" label="Region" :options="regions" />
  <HButton color="green" type="submit" block>Create project</HButton>
</HForm>

Number Field

Steppers on either side; the input stays honest numeric.

<HNumberField v-model="seats" :min="0" />

OTP Field

Auto-advance on entry, backspace walks back. Paste-friendly, digit-only.

<HOtpField v-model="code" :length="6" />

Select

The native select, dressed in card paper. No re-implemented listbox to break your screen reader.

<HSelect v-model="region" label="Region" :options="regions" />

Combobox

Type to filter, click to pick. The list is plain markup that hides itself.

<HCombobox v-model="fw" :options="frameworks" />

Autocomplete

Combobox that keeps free text — suggestions help, they never gate.

<HAutocomplete v-model="city" :suggestions="cities" />

Kbd

Keycaps set in the mono stack with a printed edge.

Press K to open the command menu, ? for shortcuts, and esc to put everything back.

Press <HKbd>⌘</HKbd> <HKbd>K</HKbd> to open the command menu.

Separator

A rule inked by hand — density breathes along its length instead of a flat 1px line.

Above the fold

Below it

<HSeparator />

Collapsible

A native details element — open state is real, keyboardable, and free.

Advanced options
Seed pinning, plate order, and misregistration live here. They come out when you need them and fold away when you don't.
<HCollapsible label="Advanced options">
  Seed pinning, plate order, misregistration.
</HCollapsible>

Accordion

Stacked collapsibles sharing hairline borders.

Why canvas and not SVG?
Ten thousand dots re-tested per frame want an immediate-mode surface. SVG would make each dot a node.
Does it SSR?
The chrome renders on the server; surfaces press on mount from the same seed, so there's no flash of unstyled grain.
What about print?
It *is* print. Canvases rasterize beautifully — that's the whole bit.
<HAccordion :items="faq" />

Dialog

The native dialog element with showModal — focus trapping and the backdrop come free.

Rename workspace

This changes the URL of every published board.

<HDialog v-model:open="open" title="Rename workspace">
  This changes the URL of every published board.
  <template #footer>
    <HButton variant="outline" @click="open = false">Cancel</HButton>
    <HButton color="blue" @click="rename">Rename</HButton>
  </template>
</HDialog>

Alert Dialog

A dialog that means it. Red ink, verb-first button, no soft exits.

Delete ink-press?

This deletes 14 boards and every published link. There is no undo.

<HAlertDialog v-model:open="open" tone="danger"
  title="Delete ink-press?" confirm="Delete forever" @confirm="destroy" />

Drawer

The same native dialog, parked against the right edge.

Press settings

Session-only overrides for this document.

<HDrawer v-model:open="open" side="right" title="Press settings">
  <HInput v-model="seed" label="Seed" />
</HDrawer>

Popover

A summary/details pair — anchored, dismissable, zero JavaScript of its own.

<HPopover label="Share">
  Anyone with the link can view this board.
  <HButton size="sm">Copy link</HButton>
</HPopover>

Tooltip

Hover or focus; pure CSS reveal. Underlined-dashed marks the invitation.

Every surface has a grainAverage dot pitch in CSS px — smaller is finer. and a seedThe integer that makes the cloud reproducible..

<HTooltip text="Average dot pitch in CSS px.">grain</HTooltip>

Preview Card

A link that shows its hand on hover.

Built with @halftone-uihalftone-uiThe component library that ships its own printing press. MIT, tree-shakeable, one canvas per part.stable and stubbornness.

<HPreviewCard href="https://github.com/ecgang/halftone-ui">
  @halftone-ui
</HPreviewCard>

Context Menu

Right-click the zone. The menu lands where the pointer asked.

Right-click here
<HContextMenu>
  <HMenuItem @select="inspect">Inspect plate</HMenuItem>
  <HMenuItem @select="copySeed">Copy seed</HMenuItem>
</HContextMenu>

Toolbar

Grouped toggles with a hairline divider — pressed means inked.

<HToolbar>
  <HToggle v-model="bold">B</HToggle>
  <HToggle v-model="italic">I</HToggle>
  <HSeparator vertical />
  <HButton variant="ghost">Link</HButton>
</HToolbar>

Pagination

Numbered pages in hairline boxes; the current one takes the accent.

<HPagination v-model="page" :total="4" />

Scroll Area

A bounded well with a thin, themed scrollbar — native scrolling underneath.

The press does not care how long the copy runs; the well stays put and the paper scrolls.

Poisson-disk sampling guarantees no two dots crowd each other, which is why the grain reads as texture instead of noise.

Every surface holds its seed, so a scrollback looks identical to the first printing.

And when you reroll, everything rerolls together — one seed to rule the page.

<HScrollArea :height="150">…</HScrollArea>

Rating

Five stars pressed from the same star field as the Marks section — click to re-ink.

<HRating v-model="stars" :max="5" />

Stepper

Numbered stations; completed steps trade their number for a tick.

1Account 2Workspace 3Invite
<HStepper v-model="step"
  :steps="['Account', 'Workspace', 'Invite']" />

Timeline

Events on a hairline rail, each anchored by a pressed dot.

v0.4.0 shipped

Ink styles and the OKLCH wheel went out together.

Charts landed

Line, pie, and radar joined the family.

First commit

One canvas, one seed, ten thousand dots.

<HTimeline :events="history" />

Ink styles

Ink styles

A second rendering mode for every component — print screens instead of the halftone field. Pass inks to any component and its surface becomes a two-ink press run: each ink gets a tonal band and its own screen angle, dots grow past their pitch into Euclidean merge blobs, and the layers composite with the plastisol blend — halfway between opaque paint and transparent process ink.

Ink surfaces print on paper (light or dark), inks stack in array order — the press sequence, so ['blue','orange']['orange','blue'] — and pattern="hatch" spreads the pair across four angled screens: ink B, ink A, their multiplied mix, and a key ink. On dark paper coverage follows lightness and inks print opaque, like screen-printing a black tee. Type is never screened — text knocks out to paper, sharp.

PropTypeDefault
inks[InkColor, InkColor] — press order; enables ink mode
pattern"stipple" | "lines" | "waves" | "hatch" | "am""stipple"
bodynumber 0–1 — 0 opaque paint · 1 transparent ink0.55
misregnumber (px of plate misregistration)1
scalenumber (screen pitch, px)6
paper"light" | "dark""light"

Studio

Every ink prop, live. Dial the press — pattern, ink pair, press order, plastisol body, plate misregistration, screen pitch, paper — on a test plate: a tonal ramp with an orb, so each screen's band is visible as it joins. plates: tuned is the harmony pass: both inks are pulled into one lightness band so neither dominates, and the mix plate's multiply is lifted into a rich overprint instead of mud. The prop line underneath is the exact component call you'd ship.

pattern
inks
body0.70
misreg1.0
scale3.4
paper
plates
<HInkPanel pattern="hatch" :inks="['blue','orange']" :body="0.7"
           :misreg="1" :scale="3.4" paper="light" />

Sign in

An auth card washed by the press — the card is an HInkPanel on dark paper, so the four-screen crosshatch prints opaque and keys white toward the base. The form stays plain; the CTA is an HButton running the same two inks.

Sign in to your pressroom
No account? Request a proof
<HInkPanel pattern="hatch" :inks="['purple','pink']" paper="dark" :misreg="1.2">
  <HInput v-model="email" type="email" />
  <HInput v-model="password" type="password" />
  <HButton pattern="hatch" :inks="['purple','pink']" :body="0.7" block>Sign in</HButton>
</HInkPanel>

Area Chart

The same HAreaChart with inks — the fill under the curve becomes the four-screen crosshatch. Depth stacks the plates: one screen at the line, blue crossing orange in the middle, their mix and the black key by the baseline. A printed chart, not a chart with a texture on it.

revenue · quarterly proof
JanMarMayJulSepNov
<HAreaChart :data="revenue" pattern="hatch" :inks="['green','blue']"
            :body="0.7" :misreg="1" :scale="3.2" />

Bar Chart

Crosshatch in the bars — each bar of an HBarChart is its own little press run: one screen at the top, the plates stacking as the bar falls toward the baseline, the tall bars reaching the black key. Value is encoded twice — height and ink.

deploys · weekly proof
W1W3W5W7W9W11
<HBarChart :data="deploys" pattern="hatch" :inks="['red','black']"
           :body="0.7" :misreg="0.8" :scale="3" />

Line Chart

An HLineChart run through the press — the inverse of the area chart. The stroke itself is the darkest pass, all four plates on the line, and the ink hangs off it like a comet tail: lighter screens falling away beneath, bare paper above.

latency p95 · rolling proof
MonTueWedThuFriSat
<HLineChart :data="latency" pattern="hatch" :inks="['pink','blue']"
            :body="0.65" :misreg="0.9" :scale="2.8" />

Donut

An HDonut gauge as a press run — the plates stack along the arc. One screen where the ring begins, the mix midway, the black key arriving right at the head of the progress. The remainder track is a whisper of the first screen.

rollout · 72% inked
72%
<HDonut :value="0.72" pattern="hatch" :inks="['red','yellow']"
        :body="0.65" :misreg="0.9" :scale="3" />

Heatmap

An HHeatmap — the contribution-graph grid, printed. Every cell is a tiny plate proof: quiet weeks take a single screen, busy weeks stack toward the key. Value maps to press depth instead of a color ramp, so the whole grid stays two inks.

commits · six-month proof
FebMarAprMayJunJul
<HHeatmap :data="commits" pattern="hatch" :inks="['blue','orange']"
          :body="0.65" :misreg="0.7" :scale="2.8" />

Ink Button

The same HButtoninks switches its fill to a press run. Hover leans on the ink. The last one prints opaque on dark paper, black-tee style.

<HButton pattern="waves" :inks="['blue','orange']">Print</HButton>
<HButton pattern="lines" :inks="['purple','pink']" :body="0.5">Preview</HButton>
<HButton pattern="hatch" :inks="['blue','orange']" :body="0.7">Overprint</HButton>
<HButton pattern="waves" :inks="['orange','blue']" paper="dark">Black tee</HButton>

Ink Meter

A meter whose fill is the crosshatch — the plates stack toward the value, the empty track keeps a faint ink dust.

Coverage72%
Ink well38%
<HMeter label="Coverage" :value="72" pattern="hatch" :inks="['purple','pink']" />

Marks

Printed sprites — an HInkMark takes a shape and runs it through the same press. The glyph is just a field: light at the crown, all four plates by the foot, so every icon carries the crosshatch. Deterministic, seeded, and re-inkable like everything else.

<HInkMark shape="bolt" :inks="['blue','orange']" :size="58" />
<HInkMark shape="star" :inks="['red','black']" :size="58" />

Comic cover

Case study — ericgang.com as a pulp cover. An HInkImage samples a photograph's luminance as the field, so the press crosshatches a real face the way a 1950s cover artist would: highlights stay paper, midtones take one or two screens, shadows stack to the key. The photo is pulled straight off the site; the type is plain DOM. Hover it — it's headed for a 3D shelf.

10¢
№ 1
COMES A CODER BEYOND CONTROL!
WITH
ERIC GANG
AS
THE FULL-STACK VISITOR
THE MAN
FROM THE
TERMINAL
<HInkImage src="/assets/eric-gang.jpg" pattern="hatch" :inks="['green','orange']"
           :body="0.62" :misreg="1.1" :scale="2.9" />

Smudge

Every press has a devil. Smudge is ours — an imp of spilled ink, four plates deep, with paper-knockout eyes and a smirk scraped clean off the plate. He haunts our empty states and 404s. Re-ink him below.

<HInkMark name="smudge" :inks="['red', 'black']" :scale="2.8" />

Utils

Palette

The seven HalftoneColor values, rendered by the engine itself.

Deploy finished