Palettes¶
This page is the catalog. For how to call the resolver (palette(),
color(), YAML shorthand, errors, smart defaults, scoring, anti-patterns)
see Palette resolver.
Families¶
Every palette belongs to one of five families. The family determines how the resolver treats the palette and what kind of data it's for.
| Family | Purpose | Used for |
|---|---|---|
sequential |
Ordered magnitude (light → dark) | Continuous numeric data without a midpoint |
diverging |
Signed values around a midpoint | Continuous numeric data with a meaningful zero or center |
categorical |
Unordered categories | Discrete enums where each value is its own identity |
scaffold |
UI structural framing | Canvas, borders, dividers, axis text; not data encoding |
tone |
Role tokens | Negative / positive / warning semantics |
Scaffolds and tones use named slots (canvas, solid, text, …), not
numbered stops. The other three families use ordered stops.
Sequential¶
Use for ordered magnitude: light at the low end, dark at the high end.
All sequential palettes ship 11 stops and downsample evenly: pass steps=N
to get N evenly-spaced stops from the spine.
WCAG note. Dark-end stops will fail WCAG AA (4.5:1) against
#222 body text. For table cell backgrounds, use
palette(name, surface="table"); see
Palette resolver,
which carves the spine at the contrast boundary and returns
AA-safe stops. Or set an explicit text_color override on dark cells.
dbt-seq-blue: default sequential¶
Single-hue blue. Default for continuous numeric data without a midpoint.
Other sequential hues¶
Same 11-stop architecture, different hue anchor. Pick by editorial fit; all share the WCAG dark-end caveat above.
| Palette | Reads as | Light end | Mid (step 6) | Dark end |
|---|---|---|---|---|
dbt-seq-amber |
Honey → deep amber | #f6edde |
#a2781a |
#2e1f00 |
dbt-seq-brown |
Cream → warm brown (pairs with paper theme) | #f3eee6 |
#9b785e |
#341911 |
dbt-seq-gray |
Pure achromatic (magnitude without hue) | #eeeeee |
#808080 |
#222222 |
dbt-seq-green |
Single-hue green | #e4f3ea |
#44926e |
#002a19 |
dbt-seq-purple |
Single-hue purple | #f5eaf8 |
#9b6ba8 |
#32113a |
dbt-seq-rust |
Cream-peach → terracotta → deep rust | #fdeae3 |
#b96648 |
#3e1000 |
dbt-seq-teal |
Teal / turquoise | #dff3f5 |
#00919d |
#00272b |
Diverging¶
Use for signed values around a midpoint: temperature anomalies, profit/loss, deviations from a baseline. All diverging palettes ship 11 stops with a neutral midpoint at step 6. Even-N downsamples auto-skip the midpoint (no gray middle); odd-N include it.
When to use which¶
| Situation | Palette |
|---|---|
| Default CVD-safe diverging | dbt-div-blue-red |
| Political-sensitivity (avoid red/blue) | dbt-div-orange-teal |
| Financial loss/gain (red negative, green positive) | dbt-div-crimson-green; must pair with icon + label |
| Climate / atmospheric data (conventional sunset ramp) | dbt-div-sunset |
| Scientific visualization (Moreland 2009) | dbt-div-coolwarm |
dbt-div-blue-red: default¶
Symmetric blue-to-red about a light midpoint. CVD-safe at common subset counts.
Other diverging palettes¶
| Palette | Low extreme | Mid | High extreme | Notes |
|---|---|---|---|---|
dbt-div-orange-teal |
#4c1f00 |
#e4e4e4 |
#003634 |
Symmetric, fully CVD-safe; non-politically-loaded |
dbt-div-crimson-green |
#540a31 |
#e4e4e4 |
#00481d |
Engineered red↔green CVD pass via lightness asymmetry; icon + label required |
dbt-div-sunset |
#125a56 |
#eceada |
#a01813 |
Paul Tol multi-hue ramp; CVD-safe; conventional in climate work |
dbt-div-coolwarm |
#3b4cc0 |
#dddddd |
#8b0000 |
Moreland 2009; canonical in ParaView/VTK and scientific viz |
Categorical¶
Use for unordered categories: series in a multi-line chart, slices in a
stacked bar, items in a legend. Categorical palettes ship a fixed number of
slots; steps=N returns the first N. There is no interpolation: stops are
what you get.
One value, one color, across the whole board¶
A palette says which colors are available. It does not, on its own, say which
category gets which one, and if each chart decides that for itself, the same
category drifts between charts. Electronics ends up blue on the bar chart
and green on the line chart, and the reader has to re-read every legend
instead of trusting the color.
dbt Charts settles the question once per board. Before any chart renders, the engine reads the executed rows, collects the distinct values of every categorical field, and assigns each value a palette slot. Every chart that draws that field then paints it from that slot, so a value keeps its color everywhere, including on charts whose data is missing some of the values.
What is shared is the slot, not the hex. Everything a chart derives from its palette is indexed the same way (the fill, the darker ink its direct labels use, the swatch in an attached table), so those stay in step with each other automatically.
Three consequences worth knowing:
- One chart's data never disturbs another's colors. A chart filtered down to two of five categories draws those two in the colors they have everywhere else, rather than sliding them up into the first two slots.
- A nested board keeps its own theme. If part of a board sets a different theme, a shared value takes the same slot in each theme's palette rather than being repainted in the outer theme's colors. The two halves agree on which position a value owns; each still looks like the theme it asked for. A chart whose own palette is too short to reach the slot a value owns is the one exception: it declines the binding and keeps the coloring it would have had, rather than wrapping around and giving two categories one color.
- It only ever re-colors a
color:channel a chart already has. A chart that identifies a category some other way (a single-metric bar withx: categoryand nocolor:) keeps the ink it has today (seesingle_series_palette). Giving it a color channel it never asked for would override authored static colors, so the binding leaves it alone.
The threshold¶
A field is bound when two or more charts on the board use it as their
color: channel. A field only one chart colors by has no
cross-chart consistency to preserve, so it keeps the chart-local coloring it
would have had. This keeps small boards from
acquiring bindings they gain nothing from.
A chart with authored layers: is the exception: it spends its color channel
on telling the layers apart, so it neither binds nor counts toward the two.
Putting layer names and category names in one legend would be the wrong
picture.
Pinning a category¶
To fix a specific value to a specific color, author it under
style.charts.category_colors, keyed by data field. The keys under values:
are the literal data values the field takes in your query results: write
them exactly as the query returns them, not a display label.
style: charts: category_colors: category: values: Electronics: category[1] Accessories: category[2] Tools: dbt-grays.muted
Values accept a palette token (resolved against the board's theme, so a theme swap re-skins the board) or a literal hex. Anything else is rejected at compile time rather than silently ignored.
A few rules follow from the block being intent rather than a cache:
- Authoring a field binds it, even on a single chart: the threshold only gates automatic binding.
- Authored entries take the first slots. Values found in the data but absent from the block are assigned after them, in first-seen order.
- A pinned color is never reused for a discovered value, so no two categories of a bound field share a swatch. Pinning two drawn values to the same color is an error rather than a silent collision.
- A pin naming a palette color pins the slot, not the ink.
category[2]and the hex thatcategory[2]resolves to mean the same thing: this value owns slot 2. A color the palette does not contain is taken literally. - A pin naming a value the render does not draw is skipped, and warns.
It claims no swatch, so nothing else shifts. dbt Charts cannot tell a
misspelled pin from a category a variable filter excluded today, so it
declines to blank the board over the difference and raises
WARN-CATEGORY-COLOR-PIN-UNSEENnaming the value instead.
Pinning is also what makes an assignment survive new data. Slots are handed out in the order values are first seen, so a category that shows up for the first time partway through a chart's rows takes the slot at that position and pushes the values after it down one. Within a single render everything stays consistent either way; pinning is what holds a color still from one week to the next.
If a field has more distinct values than the palette has swatches, what happens depends on who asked for the binding:
- You authored the field. dbt Charts raises. You named it, so quietly not honoring the pins would be the wrong kind of silence. Widen the palette, pin fewer values, or reduce the distinct count in the query.
- The engine bound it on its own. It declines instead: the field keeps the per-chart coloring it had before, which is also the only place two categories can still land on the same swatch, because each chart cycles the palette independently. Binding is something the engine volunteers; it would rather hand the board back unchanged than seat two categories on one color.
Either way the swatch count is the ceiling: nothing cycles inside a bound field.
editorial-10: default for clarity themes¶
Quieter ten-color set designed for cycling: the engine fills slots in
order without a human picker. It is the categorical family for the shipped
clarity and paper themes.
The order is blue, sky, green, purple, gold, rust, teal, brown, gray,
graphite. Every companion uses the same positions. Public roles provide stable
semantic access, including orange for rust, sage for teal, and charcoal
for graphite. cyan and moss are also accepted as names for the sky and
sage slots. Prefer roles such as category.green when hue meaning matters;
numeric slots preserve position, not semantic intent, across palette retunes.
Reach for editorial-10 (over vivid-10) when:
- The dashboard is meant to read as editorial: calmer, lower-chroma, fewer competing hues.
- Charts should accompany a dark single-series mark without competing with it.
- Cross-chart palette consistency matters and you'd rather not hand-pin per chart.
Stay on vivid-10 when:
- A chart needs up to ten categories and editorial restraint isn't a goal.
vivid-10keeps all ten hues apart for typical vision. For color-vision-deficient readers, the first five series clear the Leonardo gate (ΔE 11) and all ten keep a floor of ΔE 9; past five series, add direct labels or group the tail. - The product surface is brand-default rather than editorial.
Variants: dark, light, pale, deep¶
Categorical only. Every categorical palette -- shipped or inline
(palette: ["#4e79a7", "#f28e2b", ...]) -- derives four literal tiers
live, computed from its own stops (variant(), no companion file to
author or keep in sync). Address one with a trailing token segment, on
any addressing form:
| Form | Example |
|---|---|
| Role + alias | category.blue.dark |
| Role + slot | category[2].light |
| Palette name + slot | vivid-10.3.pale |
| Whole list | category.deep, vivid-10.pale |
The four words are literal, not a contrast direction: dark is never
lighter than its base and light never darker, on every theme. Which
variant a board or theme reaches for on a dark canvas -- so a mark stays
legible, or a mark stays deliberately recessive -- is an authoring
decision; the tier itself does not flip.
A continuous (sequential/diverging) palette's own -dark fork
(dbt-seq-blue-dark, ...) is a different, older naming convention: a
real, separately-shipped palette name, not this grammar's .dark
segment. A continuous palette's own stops already encode a monotonic
lightness order, so the variant grammar does not apply to it -- writing
dbt-seq-blue.dark raises rather than silently resolving.
| Tier | Reads as | Typical use |
|---|---|---|
dark |
A notch darker than the base, chroma held | Direct-label ink, contrast on light canvases |
light |
Moderately lighter, chroma reduced | A recessive twin that still carries its hue |
pale |
Very light, low chroma, one uniform band -- every stop converges to the same target lightness, regardless of the base | Dim-the-chorus / de-emphasis highlighting |
deep |
Very dark, chroma held, converges toward a dark pole (not a fixed band -- a darker base still ends up darker than a lighter one) | Single-series ink |
Direct-label ink (endpoint labels, stacked-bar segment names, pie slice
labels, support-table header swatches) is not sourced from the dark
variant -- the engine derives it live from each mark color against the
chart's own canvas (label_ink()), which is what keeps labels legible on
dark themes like neon with no theme-level rebind.
A few editorial-10 slots, tiers computed rather than hand-tuned:
| Slot | editorial-10 |
.dark |
.light |
.pale |
.deep |
|---|---|---|---|---|---|
| 1 | #40639c blue |
#284980 |
#7a90b4 |
#c5d2e7 |
#0e2e63 |
| 5 | #d49656 gold |
#a16723 |
#ddb793 |
#e2cdb9 |
#543000 |
| 10 | #5c6668 graphite |
#434d4f |
#8b9192 |
#ced2d2 |
#2a3335 |
single_series_palette: ink for single-series charts¶
palette (categorical, above) is the color source for charts with a color: channel; dbt Charts indexes into it once per distinct series value. single_series_palette is the separate ink source for charts that have no color: channel at all: a plain single-metric bar, line, or area. There's no series to pick a slot by value, so dbt Charts assigns each single-series chart a slot by its position in the board: the compiler walks the board tree in reading order and hands out single_series_palette[0], single_series_palette[1], and so on, wrapping back to the start once the list is exhausted. Two single-series bar charts on the same board get two different inks; the same chart re-rendered always gets the same ink, because the slot comes from tree position, not from data.
Set it at the theme or board level under style.charts, as a list of color
tokens or a palette name (expanded to that palette's stops at validation time).
Theme-relative role tokens resolve against the active theme, so
category.sky.deep selects editorial sky ink under Clarity and vivid sky ink
under Vivid.
style: charts: single_series_palette: editorial-10.deep # or a role-token list: # single_series_palette: [category.sky.deep, category.sage.deep, category.green.deep]
A chart-local style.color.static or style.color.categorical override always wins over the rhythm slot; use it when one specific chart needs to break from the cycle (for example, to match an established brand color for that metric).
The rhythm slot is unaffected by the board-wide category binding: that binding only re-colors a color: channel a chart already has, and a single-series chart has none. See One value, one color, across the whole board.
vivid-10: general-purpose ten-color¶
Blue-led and green-third: the five most chromatic hues run first, then purple,
moss, brown, and the two neutrals. Purple and moss are tuned against
color-vision deficiency rather than for chroma, which is why they follow the
lead five. Brown and charcoal sit at slots 8 and 10 because they are the only
two stops that fall under 3:1 against a near-black canvas, so a chart needs
eight series before it draws one. The default categorical palette for the
vivid and neon themes; editorial-10 (above) is the categorical family
for clarity and paper.
Every slot has all four variants: vivid-10.1.dark,
vivid-10.1.light, vivid-10.1.pale, vivid-10.1.deep. Direct-label ink is
not sourced from the dark variant -- the engine derives it live from each
mark color against the chart's own canvas.
hero-6: single dominant series¶
A six-color hero-blue-versus-neutrals palette for charts where one series should dominate. Slot 1 is the only saturated color; slots 2–6 are gray/brown neutrals that step into warmth.
Contract. The hero blue is both the most saturated and the darkest color in the palette; that's the perceptual rule that makes slot 1 win. Don't reorder; the support neutrals are designed to lie behind it.
Tonal monochrome: category-6-tonal-*¶
Single-hue categorical palettes for editorial / brand-restrained
dashboards. Five hues ship: blue, green, purple, orange, brown, each
anchored near a vivid-10 hue so they coordinate with the default
categorical system -- except purple, which keeps its own hue, about 14°
from vivid-10's purple.
| Palette | Anchor color | Reads as |
|---|---|---|
category-6-tonal-blue |
#0375c4 |
All-blue |
category-6-tonal-green |
#00875a |
All-green |
category-6-tonal-purple |
#9650a8 |
All-purple |
category-6-tonal-orange |
#b74c1f |
All-orange |
category-6-tonal-brown |
#9a642b |
All-brown (paper-theme companion) |
Contract: 4 strict-safe + 2 extended. Every tonal palette ships 6 stops with a layered contract:
| Slots | Role | CVD safety |
|---|---|---|
| 0–3 (core) | strict-safe | Color alone sufficient; pairwise CVD ΔE ≥ 11 |
| 4–5 (extended) | form-redundant only | Below the strict gate. Require dash, marker, or pattern redundancy when used in a chart |
The 6-stop cap is structural: at fixed hue, English's color vocabulary
saturates around 4–5 stops ("two blues that are 'just blue'"), and CVD
simulation collapses adjacent stops. If a chart needs 7+ categories, use
vivid-10 instead.
When to use. Editorial / branded dashboards where chromatic restraint matters more than category count. Small-multiples and KPI tiles where the color budget is tight. Charts with direct labels or stable category order ; the legend reads via labels, not by glance. Avoid for many-slice pies, dense scatters, or stacked bars with more than four segments.
Brown ↔ cream bridge. category-6-tonal-brown's extended slots (4–5)
are deliberately hue-shifted to overlap the dbt-creams scaffold region.
On the paper theme, slot 5 visually merges with the surrounding chrome;
that's the design intent. Use slot 5 only with form redundancy on paper
backgrounds.
Compatibility palettes¶
| Palette | Use |
|---|---|
tableau |
Tableau-style 10-color set. Retained for users who want the familiar Tableau hues instead of vivid-10. |
Scaffold¶
Scaffolds are UI structural framing: canvas, borders, dividers, axis
labels, table stripes, secondary text. Not data encoding. Address
them through named aliases (chrome.canvas, chrome.ink, etc.) rather
than by integer slot.
dbt-grays: neutral chrome¶
Twelve steps from near-white to near-black. The darkest step, void, is
the near-black canvas for dark themes; clarity, vivid, and other light
themes use the lighter steps for chrome.
dbt-creams: warm chrome¶
Same twelve-step shape as dbt-grays, in warm cream tones. Default
chrome for the paper theme.
Tone¶
Tone palettes encode role semantics: info, negative, positive, warning. Six
named slots per palette: bg, subtle, border, solid, solid-hover,
text. Address them through color(), not palette().
Contract. All tone slots are designed so canonical pairs (text-on-bg, solid-on-white) pass WCAG AA. Tone color alone is insufficient communication; always pair with icon + label per the standard accessibility discipline.
info: blue / information / neutral status¶
Informational notes, neutral status messages, non-error callouts.
| Alias | Hex | Role |
|---|---|---|
bg |
#e7f4ff |
Very light blue background |
subtle |
#d6ecff |
Secondary fill or hover-on-bg |
border |
#6bb9f8 |
Mid-tone divider / outline |
solid |
#008cdd |
Canonical "information" fill |
solid-hover |
#0070b3 |
Darker variant for button hover state |
text |
#003659 |
AA-contrast text color on .bg and on white |
negative: red / stop / bad¶
Critical errors, destructive actions, losses. Western convention: red = stop/bad.
| Alias | Hex | Role |
|---|---|---|
bg |
#ffedec |
Very light peach tint for surface backgrounds |
subtle |
#ffdbd8 |
Hover-on-bg or secondary fill |
border |
#e79491 |
Mid-tone divider / outline |
solid |
#94001e |
Canonical "this is negative" fill |
solid-hover |
#6e0014 |
Darker variant for button hover state |
text |
#4b000a |
AA-contrast text color on .bg and on white |
positive: green / go / good¶
Successful actions, gains, completions. Western convention: green = go/good.
| Alias | Hex | Role |
|---|---|---|
bg |
#e3f8e9 |
Very light green background |
subtle |
#cff2da |
Secondary fill / hover-on-bg |
border |
#82cb9b |
Mid-tone divider / outline |
solid |
#00884d |
Canonical "all good" fill |
solid-hover |
#006d3c |
Darker variant for button hover state |
text |
#003e20 |
AA-contrast text color on .bg and on white |
warning: amber / caution¶
Pay attention; something needs input, nothing's broken. Western convention: amber = caution.
| Alias | Hex | Role |
|---|---|---|
bg |
#fff3e5 |
Very light cream-amber background |
subtle |
#ffe7ca |
Secondary fill / hover-on-bg |
border |
#f0b871 |
Mid-tone divider / outline |
solid |
#e69812 |
Canonical "pay attention" fill |
solid-hover |
#c68100 |
Darker variant for button hover state |
text |
#643f00 |
AA-contrast text color on .bg and on white |
Further reading¶
- Palette resolver: how to call
palette()/color()from Python or YAML, plus error semantics and the anti-pattern list. - Tonal foundations: the neutral-frame thinking that pairs with the chrome scaffolds.