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/warpUsage
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
- CanvasStage, when you want another look, or your own
- DitherField, when only the background should move
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
| Prop | Type | Default | Description |
|---|---|---|---|
layout | StageLayout | 'box' | box paints its own block, scroll is a scroller and paints its view, page paints the viewport. |
children | unknown | — | What it contains. |
ramp | Ramp | — | 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). |
ground | number | — | How strong the field under the pieces is, 0 to 1. 0 leaves only the pieces. |
pattern | Pattern | — | |
cell | number | — | CSS pixels per cell. Default 3. |
speed | number | — | Multiplies the tier's speed. 0 holds the ground still; pieces still answer. |
drift | number | — | How far the field travels for each pixel scrolled: parallax. |
rail | boolean | false | A dithered scroll rail on the right, in place of the native bar. Drag it to scroll. |
veil | number | — | A dithered scrim over everything under the pieces, this dense: for a modal. |
ripple | false | Ramp | — | The inks a press rings out in, or false for none. |
painters | Record<string, DitherPainter> | — | Your own skins, drawn with the brush in tones 0 to 1 that the stage turns into inks. |
fps | number | — |
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