Dithering turns smooth values into patterns of lit and unlit pixels. It is an old print technique that gives interfaces a crisp, tactile texture. dither-kit-rs brings it to native Rust UIs.
- Deterministic
- Ordered Bayer thresholds are stateless per pixel. Nothing shimmers while animating.
- Native
- GPU quads in GPUI. Half-block glyphs in the terminal. Text stays real text.
- Small
- The core has no dependencies. Each frontend is a thin painter.
Installation
Add the frontend you need. The minimum supported Rust version is 1.95.
# Desktop
cargo add dither-gpui gpui-kit@=0.7.3
# Terminal
cargo add dither-ratatui ratatui@0.30
GPUI Kit is pinned to 0.7.3, the release gpui-motion-kit 0.5 is built against.
Agent skill
Using a coding agent? Install the dither-kit-rs skill so Claude Code, Cursor, Codex and other agents know the components, builders and conventions.
npx skills add DeepanshuMishraa/dither-kit-rs
The skill lives in .agents/skills/dither-kit-rs/SKILL.md.
GPUI Kit
Components are RenderOnce values with an id. Hover, selection, entrance and tooltip state are kept for you, so you only pass in data.
use dither_gpui::prelude::*;
DitherChart::area("visitors", vec![
Series::new("Desktop", Tone::Blue, vec![186., 305., 237., 73., 209., 214.]),
Series::new("Mobile", Tone::Pink, vec![80., 200., 120., 190., 130., 140.])
.fill(Fill::Hatched),
])
.labels(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
.bloom(Bloom::Low)
.on_hover_change(cx.listener(|this, index: &Option<usize>, _, cx| {
this.focus = *index;
cx.notify();
}))
Ratatui
Widgets are plain values. Each terminal cell holds two square pixels. Hit-test helpers turn mouse positions into data indexes.
use dither_ratatui::{Chart, prelude::*};
let chart = Chart::area(&series)
.labels(&labels)
.hover(chart.index_at(area, mouse.column, mouse.row))
.progress(ease::out_cubic(t));
frame.render_widget(chart, area);
Requires a truecolor terminal.
Components
Each one exists in both frontends. These demos run on a JavaScript port of the core, so they are the same pixels the crates draw.
Pie & donut
Hover lifts a slice. The centre shows the total.
DitherPie · PieChartRadar
Rings, spokes and dithered polygons.
DitherRadar · RadarChartSparkline
No axes or chrome. Glows on hover.
DitherSparkline · SparklineButton
The texture thickens on hover and press. Try it.
DitherButton · ButtonAvatar
32 mirrored bits and 180 hues. Same name, same avatar, in Rust and here.
DitherAvatar · AvatarGradient
A two-tone blend or a one-tone fade.
DitherGradient · GradientBackground
Animated fields for heroes and empty states. They stop when the system asks for reduced motion.
DitherBackground · BackgroundMeter
A progress bar in every fill style. Springs to new values on GPUI.
DitherMeter · MeterVocabulary
Both frontends share a small set of types from dither-core.
Tone
Seven named tones, or any hue. Each expands into a perceptual OKLCH ramp with tone.shade(level).
Fill
Bloom | Off Low High Aura: a dithered glow around lit pixels |
Matrix | Bayer2 Bayer4 Bayer8 Halftone Noise |
Field | Plasma Waves Ripple Noise Aurora |
Direction | Up Down Left Right Radial |
Motion & accessibility
- Motion. GPUI uses gpui-motion-kit: tween entrances, spring buttons and meters, a sliding tooltip, and a looping clock for backgrounds. All of it respects reduced motion.
- Buttons.
DitherButtonwraps the GPUI KitButton. Focus, Tab order, Enter and Space, the focus ring and the disabled state come from Kit. - Semantics. Charts are labelled figures, meters are progress indicators, and legend entries are focusable buttons.
- Theme. Axes, tooltips and legends use the active Kit theme tokens, in light and dark.
The core
Everything rasterizes into a PixelGrid. Paint its runs with any renderer: a game engine, an image encoder or a web canvas.
use dither_core::*;
let grid = Gradient::new(Tone::Blue).to(Tone::Purple).render(64, 32);
for run in grid.runs() {
paint(run.x, run.y, run.len, run.color);
}