dither-kit-rs

GPUI Kit · Ratatui · Rust

Dithered interfaces
for Rust.

Charts, buttons, gradients and backgrounds built from ordered dither. One engine draws the same pixels on the desktop and in the terminal.

Visitors
Last 12 months

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 · PieChart

Radar

Rings, spokes and dithered polygons.

DitherRadar · RadarChart
Revenue$48.2k +12.4%

Sparkline

No axes or chrome. Glows on hover.

DitherSparkline · Sparkline

Button

The texture thickens on hover and press. Try it.

DitherButton · Button

Avatar

32 mirrored bits and 180 hues. Same name, same avatar, in Rust and here.

DitherAvatar · Avatar

Gradient

A two-tone blend or a one-tone fade.

DitherGradient · Gradient

Background

Animated fields for heroes and empty states. They stop when the system asks for reduced motion.

DitherBackground · Background

Meter

A progress bar in every fill style. Springs to new values on GPUI.

DitherMeter · Meter

Vocabulary

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

BloomOff Low High Aura: a dithered glow around lit pixels
MatrixBayer2 Bayer4 Bayer8 Halftone Noise
FieldPlasma Waves Ripple Noise Aurora
DirectionUp 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. DitherButton wraps the GPUI Kit Button. 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);
}