tasty.gallery
Foundations
← Design
Specs
Theme

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.

When to use it
Read the usage notes to choose a component for your task.
Design tokens
See which tokens each example uses and what they control.
Check dimensions
Turn on Specs at the top right to show the 4px grid and measurements.

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.

Layout spec
ramp order
crust→mantle→base→surface0→1→2
border
1px --tasty-border-default
shadow
none (UI); modals only
Tokens used
bg-app— windowbg-sidebar— railsbg-panel— contentsurface-raised— cardssurface-hover— hover rowsurface-active— selected

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.

Layout spec
popover surfaces
search bar · tools menu · context menu · tooltip · autocomplete · multiselect menu · banner + its more-menu · modifier-hint · tutorial callout · switch-number overlay
modal surfaces
command palette · ports · remote · settings · plugins · preset editor · git viewer · clipboard viewer · DAG popup · centered dialogs
not listed
no shadow
layers
single — never stacked
color
pure black alpha only — no tinted shadow
geometry
integer px, non-negative spread, off the 4px grid on purpose
themes
one value for mocha and latte
transition
none — geometry is fixed; banner / modifier-hint fade by opacity only
Tokens used
shadow-popover— anchored overlaysshadow-modal— scrim-backed surfacesscrim-bg— the dim behind a modal

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.

Tokens used
text-primary— headings, activetext-secondary— body, labelstext-muted— meta, IDstext-disabled— disabledtext-placeholder— empty inputs

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.

Layout spec
fill text
--tasty-text-on-accent
focus ring
2px --tasty-accent-primary
agent rule
mauve = always agent
Tokens used
accent-primary— blueaccent-info— skyaccent-success— greenaccent-warning— yellowaccent-attention— peachaccent-danger— redaccent-agent— mauve

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).

Layout spec
set
16 ANSI (8 normal + 8 bright)
source
derived from the Catppuccin palette
scope
terminal cells — not UI chrome
state fills
selection · vi cursor · search
Tokens used
ansi-red— SGR 31 redansi-green— SGR 32 greenansi-bright-blue— SGR 94 br.blueselection-bg— selection fillsearch-match-active-bg— active match

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.

Mocha: a match and the match under the cursor
Latte: a match and the match under the cursor

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.

Layout spec
UI cap
--tasty-font-size-max
heading
body size, weight 600
mono
--tasty-font-mono
Tokens used
font-ui— chrome (system sans)font-mono— D2Coding — code/termfont-size-body— 13px defaultfont-size-caption— 11px metafont-weight-semibold— 600 = heading

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.

Layout spec
grid
4 / 8 / 12 / 16 / 24
rule
multiples of 4px only
heights
snap to grid too
Tokens used
space-xs— 4 · icon gapsspace-sm— 8 · control gapsspace-md— 12 · card padspace-lg— 16 · sectionsspace-xl— 24 · regions

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).

Layout spec
radius
4px (--tasty-radius)
border
1px (--tasty-border-width)
UI motion
90–120ms --tasty-ease-ui
terminal motion
0ms — always
Tokens used
radius— 4px everywhereradius-sm— 2px innermotion-ui— 120ms feedbackmotion-term— 0ms — terminalfocus-ring-width— 2px ring

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.

Layout spec
stops
0.8 / 1.0 / 1.2 (--tasty-ui-scale-sm/md/lg)
active value
--tasty-ui-scale
consumed by
sidebar root zoom
excluded
title bar, tabs, panes, dialogs
control
Appearance › Display
Tokens used
ui-scale-sm— 0.8 · compactui-scale-md— 1.0 · defaultui-scale-lg— 1.2 · roomyui-scale— active — the only one read

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.

Layout spec
new semantic roles
border-frame · glyph-dim · accent-decorative
new component role
statusbar-theme-glyph (→ glyph-dim)
moves (pixel changes)
C3 · C4 · C5 · C6
Latte
all four new roles track the ramp — no per-theme remap
contrast
C3 / C4 are disabled ink — exempt from 4.5:1; C2 is chrome, not text
Tokens used
border-frame— divider / frame edgeglyph-dim— dim chrome glyphsaccent-decorative— header ornamenttext-disabled— every disabled label / glyph

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.

Layout spec
popup frame
--tasty-border-frame
popup titlebar underline
the same — it is part of the frame
popup inner dividers
unchanged — --tasty-border-strong, one step below
pane divider
border-frame — pixel unchanged
GPU-inactive surface border
border-frame — pixel unchanged
Mocha frame vs panel
1.80:1 → 2.46:1
Latte frame vs panel
1.60:1 → 1.91:1
4.5:1
not applicable — a 1px structural edge is not text; the move raises separation in both themes
transcription fix
C1 reads “new role; the popup frame moves one step up”, not “pixel-unchanged”
fifth call site
Settings › Appearance inactive card border is NOT a frame — it is a line inside a panel → --tasty-border-strong (neutral-500 → 400, pixel changes). The frame list stays at four.
Tokens used
border-frame— popup frame · titlebar underline · pane divider · GPU-inactive surfaceborder-strong— dividers INSIDE a surface

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.

Layout spec
rule
read text → snap up · numeric micro-label → snap down
scale used
13 body · 12 term-sm · 11 caption · 10 micro
new primitive
font-size-30 (branding exception, named)
glyphs
judged on the icon family, not the type scale
zoom
integers at 0.85 / 1 / 1.2 — no .5 survives round()
Tokens used
font-size-brand-display— first-run brand title (30)font-size-caption— 11 — hintsfont-size-micro— 10 — badge counts

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.

Layout spec
fill
0.12 — --tasty-tint-fill-alpha
edge
0.36 — --tasty-tint-border-alpha
retired
0.14/0.45 · 0.12/0.35 · 0.11/0.36
partial uses
fill-only · border-only (same coefficients)
affected
file-picker info badge · keybinding IE notice & migrate card · Plugins error box / Installed badge / Attention banner · warning callout · remote & script badges · chip remote tag
clipboard inset (I1)
14 → 12 (--tasty-space-md) — the 4px grid wins; no 14 semantic is opened
Tokens used
tint-fill-alpha— every tinted filltint-border-alpha— every tinted edgespace-md— clipboard header / type-bar / footer inset

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.

Layout spec
A · target
none — WCAG exempts disabled
rule 1 · order
placeholder < disabled < muted
rule 2 · parity
Latte remap n700 → n800
B · ground
n/a (no target) — report on the control's own ground
C · enabled arrow
in scope → text-muted (3:1 non-text)
one ink
labels + glyphs, unchanged principle
pixels
Latte disabled everywhere · both themes' enabled arrow
Tokens used
text-disabled— Mocha n700 · Latte n800tab-scroll-arrow-fg— → text-mutedtab-scroll-arrow-fg-disabled— → text-disabled

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.

Layout spec
A centre glyph
22 → 24 · --tasty-center-state-glyph-size
B scripts glyph
26 → 24 · merged with A
C block height
100 / 120 → none (centres in region)
D toolbar 32
→ --tasty-toolbar-height (new semantic)
D scroll max 200
tutorial topic list · no DTCG token · hand-written Theme accessor · scales
D col min 200
withdrawn — = port Process col (D7)
E popup sizes
outside tokens · 350 → 352 · 150 → 152
ui_scale
A–D scale (incl. the tutorial cap) · E popup sizes do not
Tokens used
icon-size-lg— → size-24 (new tier)toolbar-height— → size-32 (new)git-toolbar-height— → toolbar-height

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.