Design

One palette, one status vocabulary, four key tiers, and a defined fallback for every terminal. This page is the reference for all four.

Colour

The palette is warm amber on cool slate. It nods to GitLab's tanuki without using GitLab's brand orange.

Components name a role, never a hex value. Colour literals live in the theme package and nowhere else, so a token change reaches every screen at once.

Surfaces and borders

bg.base#0F1117
bg.surface#161922
bg.overlay#1E222D
bg.selected#232838
border.faint#262B38
border.default#39404F
border.focus#E8A33D · 8.75:1

Text

text.primary#E6E9EF · 12.06:1
text.secondary#A8B0C0 · 6.73:1
text.muted#8A94A7 · 4.80:1

Meaning

accent.primary#E8A33D · 6.80:1
accent.secondary#7AA2F7 · 5.82:1
status.success#7BD88F · 8.42:1
status.warning#E8C55D · 8.78:1
status.error#F07178 · 5.12:1
status.running#5FD7E0 · 8.58:1
status.pending#C4A7F5 · 7.12:1
status.neutral#8A94A7 · 4.80:1

Contrast

Every token clears WCAG AA — 4.5:1 for text, 3:1 for interface elements — against every surface, not only against the frame background. Text sits on four backgrounds, and the one that decides the palette is the selected row: a timestamp there is the most-scanned cell in the product. The ratio beside each swatch is that worst case, computed on every commit rather than eyeballed on one monitor. The lowest is text.muted at 4.80:1, and that headroom is what a small tweak spends.

Two tokens are exempt, and each says why. border.faint and border.default carry no state: a row separator at 3:1 draws a grid louder than the rows, and an unfocused pane's edge means nothing on its own. border.focus is what identifies focus, and it clears 3:1 several times over.

The contrast theme re-derives the whole palette at AAA, 7:1 for body text, on near-black surfaces — 7:1 for every hue is not reachable above a slate background.

Run labdash theme preview to see these numbers computed in your own terminal.

Light theme

The same token names, different values, verified by the same computation.

accent.primary#8A4F00 · 5.35:1
accent.secondary#2B58BC · 5.31:1
status.success#106B36 · 5.39:1
status.warning#7A5600 · 5.42:1
status.error#B32E38 · 5.09:1
status.running#0C6169 · 5.85:1
status.pending#603AAB · 6.36:1
status.neutral#5F6778 · 4.63:1

Capability tiers

labdash reads the terminal's capability once at startup and stores it on the theme, so no call site branches on colour depth.

TierDetected byWhat happens
MonochromeNO_COLOR, or TERM=dumbNo colour. Hierarchy from glyphs, weight and layout
16 ANSITERM without 256colorIndices 0–15. Your terminal's own theme supplies the hues
256TERM contains 256colorNearest match into the 256-colour cube
True colourCOLORTERM is truecolor or 24bitThe palette above, exactly

At the 16-colour tier the selected row is reverse video rather than a background colour, because reverse video is the one background every terminal theme renders reliably.

NO_COLOR is honoured unconditionally, and no setting overrides it. --no-color does the same for a single run. At either, every state is still identifiable, because colour never carries a state on its own.

The status vocabulary

Every state is a triple: glyph, colour, word. Remove any one and the other two still carry it.

labdash · every pipeline status
The same screen in ASCII mode has identical column boundaries.

Twelve of those rows are GitLab's pipeline states. Two are labdash's: a merge request with no pipeline at all, and a job that failed with allow_failure: true.

GitLab has thirteen states and this table has twelve rows, because the mapping is many-to-one where the difference is not something you can act on: WAITING_FOR_RESOURCE and WAITING_FOR_CALLBACK both read waiting.

A glyph repeats where the word separates two states. covers pending and preparing, covers canceled and no pipeline. A word never repeats, which is what makes a state identifiable by its glyph and its word together, never by the glyph alone.

An allow-failure job that failed renders as a warning. Red is reserved for the failures that stop a pipeline, because a dashboard that shows red for something the pipeline was told to tolerate teaches you to stop reading red.

Merge blockers use the same triple. Each value of GitLab's detailedMergeStatus maps to a phrase, a token and a glyph: ready, conflicts, ci failed, ci running, needs 2, 3 threads open, needs rebase, changes requested, policy blocked. A value labdash does not recognise is title-cased and logged rather than dropped.

Icons

No patched font is required. The glyphs above come from ordinary Unicode blocks that a terminal font has carried for years, so there is nothing to install and nothing to detect.

Every glyph occupies one cell, and every ASCII fallback is one character, so a screen rendered either way has identical column boundaries. A glyph that is one cell in one mode and two in the other is a layout bug.

theme.icons takes auto, unicode or ascii. In ascii mode no byte above 0x7F reaches the screen, borders are + - |, and the spinner is |/-\.

auto reads your locale. Three of the pipeline glyphs — running, created, canceling — are classed as East Asian Ambiguous, as are several of the markers, meaning one cell in most terminals and two in a terminal configured for CJK text. On a CJK locale auto chooses ascii, because a glyph drawn two cells wide where one was measured shifts every column after it. Set icons: unicode to keep the glyphs anyway.

labdash theme preview --icons=ascii renders the whole vocabulary the other way. Hold one screenshot over the other: the columns line up exactly, and that is the property a test asserts on every commit.

Type and borders

There is one font: yours. What labdash controls is weight, case and glyph choice.

DeviceUsed forNever
BoldThe selected row's title, section headings, key capsWhole rows, body text
FaintStale rows, disabled actions, muted textAnything read carefully
ReverseSelection, at the 16-colour tier onlyAs decoration
UnderlineNothingTerminals disagree, and it collides with links
ItalicNothingMany terminals render it as reverse video
UPPERCASEColumn headers, the view nameData, ever

Borders use ╭ ╮ ╰ ╯ ─ │; rounded reads softer without costing a cell. A focused pane changes colour and never geometry, because a border that thickens on focus shifts every column beside it.

Space

NameColumnsBetween
tight1A glyph and its label
normal2Table columns
loose4Logical groups in the footer

Vertical space costs more than horizontal: one blank row between preview sections, never two, and no blank rows inside a table. Chrome takes four rows, the filter bar takes one more while it is open, and the rest is data.

Responsive columns

The table decides its own layout at the width it is given.

  • title, blockers and pipeline are present at every width.
  • Every other column carries a priority and a minimum width, and drops in that order as the terminal narrows.
  • Text is left-aligned, numbers and durations right-aligned, a single-glyph status centred.
  • Truncation is always , never a hard clip, and a wide glyph never shifts a column.

The preview pane takes 34 to 40% of the width. auto puts it on the right at 120 columns or wider, and along the bottom below that.

Keys, in four tiers

TierContains
L0 universal Enter Esc q ? b f P
L1 vimj k h l g G /
L2 actionsOne mnemonic key per verb
L3 powerChords

L0 and L1 are complete alternatives to each other, not a core plus an add-on. Nobody is locked out for not knowing vim, and nobody who does has to reach for an arrow key. Every action is reachable from the keyboard; the mouse only ever adds.

Bindings are fixed. The coherence rules that make an unlearned key guessable are on the keyboard reference.

Motion

Terminals redraw whole cells, so motion is cheap to add and expensive to live with.

AnimateDo not animate
A spinner on running workRow insertion or removal
A progress meter with real progressScrolling
The splash gradient, onceSelection movement
A toast appearing and dismissingColour transitions
RuleValue
Spinner frame rate10 fps
Toast lifetime4 s, or any key
Splash900 ms maximum, skipped by any key
Any transition150 ms, or nothing

theme.reducedMotion: true replaces the spinner with a static marker and drops the gradient. State is still communicated: the task line still names what is running.

Copy

Terse and factual. The user is mid-task and something failed. Say what happened and what to do.

DoDo not
"gitlab.example.com timed out after 15s.""Oops! Something went wrong 😅"
"Your token expired on 2026-07-30.""Uh oh, we couldn't authenticate you!"
"Nothing needs your review.""You're all caught up! 🎉"
"needs 1 approval""This MR is not yet approved by a sufficient number of reviewers"

No exclamation marks. No emoji in prose, because glyphs are part of the status vocabulary and that is a different thing. Never blame the user, and never say "invalid" when "not recognised" is true.

Every message follows a formula:

KindFormulaExample
Errorwhat failed, then why, then what to doplatform/* could not refresh — gitlab.example.com timed out after 15s. Press r to retry.
Emptynothing here, restating the query. one actionNothing needs your review. This is currentUser.reviewRequested, open and non-draft.
Confirmverb object, consequence?Merge !2841 into main, squashing 7 commits and deleting feat/retry-budget?
Blockedcannot verb, then the specific reasonCannot merge — 2 threads are unresolved.

GitLab's words, not ours: "merge request" never "pull request", "pipeline" never "build", "To-Do" capitalised as GitLab does. Someone who searches GitLab's documentation for a word they saw here finds it.

Identity

The wordmark is figlet's ANSI Shadow, generated once and committed as a text file, so there is no font dependency and no startup cost.

labdash · the wordmark

The two-tone split is the identity: lab recedes into text.muted, and dash takes a horizontal gradient from accent.primary to status.error, amber to coral. It degrades by tier:

TierTreatment
True colourThe full gradient, interpolated per column
256Three fixed stops
16lab in ANSI 8, dash in ANSI 3
MonochromePlain: lab dim, dash bold
ASCII modeThe figlet Standard art below
labdash · ASCII fallback
No byte above 0x7F. For serial consoles, CI logs, and restrictive corporate terminals.

The gradient lives on the splash and nowhere else. It never touches body text.

The mascot

GitLab's own logo is a tanuki, a Japanese raccoon dog, so the correct nod is 🦝 rather than a fox. labdash draws its own, and uses neither GitLab's logo, nor its brand orange, nor any wording that implies endorsement.

This site follows the same rules

Every colour on these pages is one of the tokens above, and the terminal screens are rendered with the shipped palette.