Foundations
See how Tasty's colours, spacing, and typography work in context. Each example shows where a token is used and what it controls.
This page uses a saved copy of the design system, last changed on . It may differ from the current design source.
Color — elevation (surface ramp)
Depth reads through surface tint, never shadow
Tasty has no shadow system (modals aside). Pick the next step up the ramp whenever you nest a container. A card on a panel is --tasty-surface-raised on --tasty-bg-panel — one step, not a drop shadow.
Do tint up exactly one ramp step each time you nest a container.
Don't add a box-shadow to cards or panels — only floating surfaces use shadow (next section).
Elevation — floating surfaces (the two shadows)
Two shadows, and the rule that picks one
In-page depth is tint. A surface that leaves the page gets a shadow, and there are exactly two — no third value, no per-component shadow. The rule is the surface's relationship to the ground: anchored and scrim-less (it floats over live content it doesn't own) takes --tasty-shadow-popover; centered and scrim-backed (it owns the viewport) takes --tasty-shadow-modal — and the scrim. The scrim dims the ground but draws no edge, so on a dark theme a dark modal on dimmed dark ground loses its silhouette; the shadow is what carries the edge.
Do reference the token. Every floating surface in this system resolves to one of these two values — a hand-written box-shadow is how a third shadow gets into the product.
Don't put a shadow on an anchored surface that already sits inside a shadowed one (a menu opened inside a modal), and don't stack layers to make a "bigger" lift.
One sanctioned exception lives outside this pair: --tasty-titlebar-csd-shadow (0 18px 50px -8px) is OS window-frame chrome, and its negative spread is functional — it keeps the falloff inside the 8px band that carries the resize edges.
Color — text
Hierarchy by text color, on any surface
Four text tints carry hierarchy. Headings aren't bigger — they're --tasty-text-primary at weight 600 (see Type). Meta, captions, and IDs drop to --tasty-text-muted.
Color — accent roles
Accents map to roles, not decoration
Each accent owns one meaning. The standout rule: mauve = AI agent. Anything agent-driven is tinted mauve so user-vs-agent stays visible at a glance. Don't reach for an accent for looks — reach for the role.
Color — terminal / ANSI palette
The colors a terminal cell paints with — not UI chrome
These are the terminal content colors the renderer writes into cells, kept deliberately separate from the accent roles above. An app's SGR 32 green is --tasty-ansi-green — it is not --tasty-accent-success, even though both are green. Never wire terminal output to a UI accent, or a theme tweak to one will silently repaint the other. The 16 ANSI colors derive from the same Catppuccin palette; the four state fills are what Tasty itself paints onto cells (selection, vi cursor, search).
Active match ink (2026-10-10 b12). Glyphs inside the active match are drawn in --tasty-search-match-active-fg, not the cell's own foreground: Mocha's light text on yellow 70% was 1.61 : 1. Mocha ink → neutral-200 (base) 6.95 : 1. Latte keeps text-primary ink and drops the active fill to 50% → 4.68 : 1 (70% was 3.94). Ordinary matches (30%) keep the cell's own colours: Mocha 4.75 · Latte 5.50. Cell colours set by the app (SGR) are overridden only inside the active match.
Don't use an ANSI color for UI chrome or an accent for terminal output — ansi-green and accent-success are different roles that only happen to share a hue.
Type
Two families, hard 14px cap, hierarchy by weight
UI chrome uses the native system sans, capped at 14px — body is 13px, captions 11px. Code and terminal use D2Coding (the brand mono, with ligatures). Headings are body-size at weight 600; never scale UI text up for emphasis.
Two sanctioned exceptions to the 14px cap, both flagged in the tokens: --tasty-font-size-prose-h1 (20px — rendered markdown content, not chrome; since the egui_commonmark adoption this is the Heading anchor at the top of the library's interpolated heading ladder, not a per-level size) and --tasty-font-size-brand-wordmark (17px — the mono “tasty.” sidebar wordmark, a branding mark). Nothing else in UI chrome exceeds 14px. Markdown content typography is otherwise library-driven (size ladder + leading fixed by egui_commonmark) — see the Markdown viewer specimen.
Spacing — the 4px grid, in use
Five steps, each with a job
The whole system snaps to a 4px grid — there are only five spacing tokens. Below, each one is shown doing its job, not as an abstract bar. Turn on Specs to see the 4px rhythm under each example.
Don't use values off the grid (6px, 10px, 14px). If something needs in-between, you're missing a layout, not a token.
Radius · border · motion
Crisp and rectilinear — it's a terminal
Radius is always 4px (2px for tiny inner chips). Borders are always 1px. Motion is short UI feedback only — and terminal content never animates (0ms, always).
UI scale — sidebar zoom
One multiplier scales the sidebar; everything else stays fixed
Sidebar scale is a single multiplier exposed in Settings › Appearance › Display. Three named stops feed one active value — components read only --tasty-ui-scale, never the stops directly. It zooms the sidebar alone (wordmark, workspace list, footer); the title bar, tabs, panes, and dialogs are untouched, and the terminal keeps its own glyph size (a separate font-size axis). Bigger nav targets without disturbing the work area.
Don't read a named stop (--tasty-ui-scale-lg) in a component, and don't add per-component size tokens (no "sidebar = medium"). One multiplier, read in one place, the 4px grid intact.
Implementation: the sidebar root sets zoom: var(--tasty-ui-scale) with height: 100% so it fills top-to-bottom at any scale. Nothing else reads --tasty-ui-scale — the title bar, tab strip, panes, and modals render at fixed size, so terminal columns and tab hit-targets never shift.
Role gaps — settled
Colors that had no role of their own
Seven places were reading a role whose value matched but whose meaning didn't. Four get a name, three move to the role they should always have used. New roles preserve the pixel; moves change it, and each is called out.
The three value-preserving holdouts elsewhere (tab hover fill, tab separator, toast border) are confirmed as intended visual changes — move them to canonical; they were never design questions.
C1 correction — the popup frame does move, and should (settled)
C1 was transcribed as “pixel-unchanged”. That holds for two of its three places: the pane divider and the GPU-inactive surface border came from --tasty-surface-active, the same ramp step. The popup frame came from --tasty-border-strong one step below, so it moved — neutral-400 → neutral-500. The move is the correct outcome, not a regression. A frame is the edge between a floating surface and everything behind it and needs more separation than a line inside that surface; the popup's internal dividers stay on --tasty-border-strong exactly one step below, so frame and content now read in the right order. The step is chosen for contrast, not for lightness: in Mocha neutral-500 is brighter than the panel, in Latte it is darker. Both gain.
Don't mint a popup-only border role to put the old value back. Three places, one meaning — the edge that bounds a frame — and a fourth role would only record that one of them used to be wrong.
Frame vs partition, stated once. --tasty-border-frame bounds a surface against what is behind it: popup frame · popup titlebar underline · pane divider · GPU-inactive surface border. Anything drawn inside a panel that separates siblings — dividers, card outlines, section rules — is a partition and reads --tasty-border-strong (or --tasty-border-default when quieter). The appearance-tab card border arrived on border_frame() only by a value-preserving port from surface_active(); it moves down one step so the outer-edge > inner-line order holds inside Settings too. The active card keeps its accent edge.
Half-pixel type sizes snap to the scale
A .5 font size can never equal a token: UI sizes pass through round() at every zoom. All of them snap, by one rule — text you read snaps up, numeric micro-labels snap down.
One tinted-box recipe: 12% fill, 36% edge
The same “accent as a soft box” idiom had drifted into four coefficient pairs (0.14/0.45, 0.12/0.35, 0.11/0.36, 0.12/none). They converge on one public pair — --tasty-tint-fill-alpha 0.12 and --tasty-tint-border-alpha 0.36 — with two sanctioned partial uses: fill only (warning callout) and border only (the remote chip tag). Every accent uses the same two numbers, so a tinted box is recognisable across the app regardless of hue.
Opacity coefficients are primitives (--tasty-opacity-tint-fill / -border) with semantic aliases, so a per-role override stays possible later without re-scattering literals.
Disabled ink — no contrast target, Latte one step up (2026-09-28)
WCAG 1.4.3 and 1.4.11 exempt disabled controls, and a 4.5:1 target would collapse the hierarchy: in Latte the first ramp step that clears 4.5:1 on the panel is neutral-900, which is already --tasty-text-muted. So disabled gets no contrast target. Two rules replace it. Order: disabled sits strictly below --tasty-text-muted and above --tasty-text-placeholder on the ramp, and the ink step is the only thing that marks disabled (no extra opacity on the label). Parity: the Latte ramp is compressed at its light end, so --tasty-text-disabled remaps to neutral-800 in Latte (Mocha stays neutral-700). Labels and glyphs still share the one ink. The enabled tab-strip arrow is an active control glyph and moves to --tasty-text-muted, which clears 3:1 in both themes. Ratios below are flat colour on the control's own ground, and flat colour is the basis of the 3:1 (2026-09-29); rendered AA glyphs measure lower and are not part of the target. Disabled controls take this ink with no opacity (Components › Buttons › Disabled). Arrow shape: Layouts › Pane tab strip.
Don't lift disabled to 4.5:1. In Latte that lands on text-muted and disabled stops reading as disabled. The hierarchy is the requirement; contrast is reported, not targeted.
Structural dimensions — the leftovers (2026-09-28)
Four items with no design answer, settled. A · B: the centre glyph 22 and the scripts empty glyph 26 become one CenterState part on a new icon tier, --tasty-icon-size-lg = 24 (Components › CenterState). C: no nominal block height; the block centres in its list region. D: a 32px in-surface toolbar is a container role, so it gets --tasty-toolbar-height and --tasty-git-toolbar-height re-points to it (value unchanged). Scroll max-height 200 stays a screen-only dimension, no DTCG token: it is the tutorial topic popup's topic-list cap (Overlays › Tutorial). It bounds rows that scale, so it scales with ui_scale; a hand-written Theme accessor (not generated from DTCG) is the right holder (2026-09-29). Column min-width 200 is withdrawn: it was an example from the request, and the only such site (port scanner Process column) already has --tasty-port-process-col-min-width (D7). Borrowing --tasty-field-width-lg or --tasty-settings-sidebar-width stays a false coupling. E: popup default sizes stay outside the token system. They are per-popup screen values; a size tier would imply they are interchangeable. Values design owns are fixed below, snapped to the 4px grid.
Moving A · B · D onto tokens puts them on ui_scale: at 0.85 / 1.2 the pixels change (glyph 20.4 / 28.8). That is intended — they are UI chrome, not screen frames.