Skip to content
weftGitHub

DitherStage

A CanvasStage in the dithered look: every skin drawn in whole cells of the theme's inks.

SourcePackage @weft-js/warp

Preview

tsx
import { DitherStage, Grid, Row, Stack, StageSelect } from '@weft-js/warp'export function StageSkins() {  return (    <DitherStage kind="drift" ground={0.35} style="padding:28px"><Skins group="look-dither" /></DitherStage>  )}const min = 60_000function Skins(props: { group: string }) {  return (    <Grid min="md" cols={2} gap="group">      <Stack gap="related" data-skin="card">        <Row gap="related" wrap>          <button data-skin="button">Deploy</button>          <button data-skin="button" data-look="outline">Preview</button>          <button data-skin="button" disabled>Paused</button>        </Row>        <label data-part="label">Project<input data-skin="field" value="storefront" /></label>        <label data-part="label">Region<StageSelect options={['eu-west', 'us-east']} /></label>      </Stack>      <Stack gap="related" data-skin="card">        <label data-part="option"><input type="checkbox" data-skin="check" checked />Preview every push</label>        <Row gap="group">          <label data-part="option"><input type="radio" name={props.group} data-skin="check" checked />Node</label>          <label data-part="option"><input type="radio" name={props.group} data-skin="check" />Bun</label>        </Row>        <label data-part="option"><input type="checkbox" role="switch" data-skin="switch" checked />Deploy on push</label>        <label data-part="label">Build minutes<input type="range" data-skin="range" value="60" /></label>        <Row gap="tight" wrap>          <button data-skin="chip" aria-pressed="true">Live</button>          <button data-skin="chip" aria-pressed="false">Building</button>        </Row>      </Stack>    </Grid>  )}

Installation

npm install @weft-js/warp

Usage

tsx
import { DitherStage } from '@weft-js/warp'<DitherStage kind="drift" ground={0.35} style="padding:28px"><Skins group="look-dither" /></DitherStage>

Hybrid and your own

Anything without a skin paints itself over the stage, so warp components and canvas pieces share a page.

Warp components on a stage

The card on the left is warp's; the one on the right is drawn in cells, with dithered type and a live art field.

tsx
import { Badge, Button, Card, DitherStage, Row, Split, Stack, Text } from '@weft-js/warp'export function StageHybrid() {  return (    <DitherStage kind="flow" ramp="info" style="padding:28px">      <Split side="end" width="list" gap="group">        <Card title="storefront" eyebrow="Live in eu-west">          <Stack gap="related">            <Text subtle>412 deploys, 99.98% uptime, 84 ms at p95.</Text>            <Row gap="related"><Button>Open</Button><Button emphasis="outline" tone="neutral">Logs</Button><Badge tone="positive">Healthy</Badge></Row>          </Stack>        </Card>        <Stack gap="related" data-skin="card">          <h3 data-skin="type" style="font-size:28px">Drawn in cells</h3>          <div data-skin="art" data-kind="rings" role="img" aria-label="Rings spreading over a dithered field"></div>          <button data-skin="button">Promote</button>        </Stack>      </Split>    </DitherStage>  )}

A skin of your own

painters maps a skin to a function that draws its piece with the brush, in tones that the stage dithers. This battery reads a <meter>.

tsx
import { state } from '@weft-js/core'import { DitherStage, LEVEL, Stack } from '@weft-js/warp'import type { DitherPainter } from '@weft-js/warp'export const StageOwnSkin = () => {  const charge = state(64)  return (    <DitherStage ground={0.25} painters={PAINT_BATTERY} style="padding:28px">      <Stack gap="related" align="start">        <meter data-skin="battery" data-ramp="positive" min="0" max="100" value={charge.value} aria-label={`Battery at ${charge.value}%`} style="inline-size:180px;block-size:72px"></meter>        <label data-part="label">Charge {charge.value}%<input type="range" data-skin="range" data-ramp="positive" value={charge.value} onInput={(e: Event) => (charge.value = +(e.currentTarget as HTMLInputElement).value)} style="inline-size:240px" /></label>      </Stack>    </DitherStage>  )}const min = 60_000const battery: DitherPainter = (p, b) => {  const nub = Math.max(2, Math.round(p.h / 4)), body = p.w - nub - 1, bars = 5, gap = 1, inner = body - 4, bar = (inner - gap * (bars - 1)) / bars  b.ring(p.x, p.y, body, p.h, 2, 1, LEVEL.ink)  b.fill(p.x + body, p.y + nub, nub, p.h - nub * 2, 0, LEVEL.ink)  for (let i = 0; i < bars; i++) {    const lit = p.value * bars - i    b.fill(p.x + 2 + i * (bar + gap), p.y + 2, bar, p.h - 4, 0, lit >= 1 ? LEVEL.tone : lit > 0 ? lit * LEVEL.tone : LEVEL.soft * p.hover)  }}const PAINT_BATTERY = { battery }

A modal

A native <dialog> with its own stage: veil dithers over the page, and the panel rises in each time it opens.

tsx
import { DitherStage, Row, Stack } from '@weft-js/warp'export const StageModal = () => {  let dialog: HTMLDialogElement | undefined  return (    <DitherStage ground={0.3} style="padding:28px">      <button data-skin="button" onClick={() => dialog?.showModal()}>Rename project</button>      <dialog ref={n => (dialog = n)} aria-label="Rename project">        <DitherStage layout="page" veil={0.5} ground={0} onClick={(e: MouseEvent) => e.target === e.currentTarget && dialog?.close()}>          <form method="dialog" data-skin="panel">            <div data-skin="bar"><span>Rename project</span></div>            <Stack gap="related">              <label data-part="label">Name<input data-skin="field" value="storefront" autofocus /></label>              <Row gap="related" justify="end">                <button data-skin="button" data-look="outline" value="cancel">Cancel</button>                <button data-skin="button" value="save">Save</button>              </Row>            </Stack>          </form>        </DitherStage>      </dialog>    </DitherStage>  )}

When to use

Use it for

  • A page or section in the dithered look, ready made

Reach for something else

Accessibility

  • Everything CanvasStage promises holds: real controls, plain borders before it paints and under forced colours.
  • Reduced motion holds the field still and drops the dissolves and rings; every state still shows.

API Reference

PropTypeDefaultDescription
layoutStageLayout'box'box paints its own block, scroll is a scroller and paints its view, page paints the viewport.
childrenunknown—What it contains.
rampRamp—The field's inks, and the ramp of pieces that name none.
kind"weave" | Field | "flow" | "rings" | "aurora" | "drift"—The ground: a named field or your own function of (x, y, t).
groundnumber—How strong the field under the pieces is, 0 to 1. 0 leaves only the pieces.
patternPattern—
cellnumber—CSS pixels per cell. Default 3.
speednumber—Multiplies the tier's speed. 0 holds the ground still; pieces still answer.
driftnumber—How far the field travels for each pixel scrolled: parallax.
railbooleanfalseA dithered scroll rail on the right, in place of the native bar. Drag it to scroll.
veilnumber—A dithered scrim over everything under the pieces, this dense: for a modal.
ripplefalse | Ramp—The inks a press rings out in, or false for none.
paintersRecord<string, DitherPainter>—Your own skins, drawn with the brush in tones 0 to 1 that the stage turns into inks.
fpsnumber—

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="stage"] [data-part="paint"]

paint

RelatedCanvas stageDither field