Modernize GitLab's data visualization color system
## About
GitLab's Pajamas data visualization palette is well-founded but now trails industry leaders in several critical areas: **token architecture, scalability beyond 5 hues, OKLCH-based palette generation, pattern fill support, and interactive state definitions**. IBM Carbon, Atlassian, and GitHub Primer have each raised the bar since Pajamas was last updated, and the convergence on perceptually uniform color spaces, three-tier token models, and redundant encoding makes this an opportune moment for a structured upgrade. This report delivers specific, implementable recommendations across eight dimensions — with hex values, OKLCH coordinates, contrast ratios, and architectural patterns drawn from the best systems operating in production today.
---
## How GitLab compares to the industry's best systems
GitLab's 5-hue, 55-variable system sits in the middle of the pack. **IBM Carbon leads with 14 categorical colors**, 4-hue sequential scales with 10 steps each, 2 diverging palettes, and an alert palette — all documented with specific hex values for both light and dark themes. Atlassian deploys **94 design tokens** dedicated to data visualization with three emphasis levels per hue, hover state tokens, and automatic light/dark switching. GitHub Primer offers **17 mark colors** plus muted variants, mandates different stroke styles per line series, and — uniquely — ships dedicated color vision deficiency themes for tritanopia and colorblindness alongside high-contrast modes.
Stripe pioneered perceptually uniform palette construction using **CIELAB**, creating 10 hues × 10 lightness steps where any two colors 5+ steps apart are guaranteed to pass WCAG text contrast. Shopify's Polaris Viz uses **HSLuv** for theme generation with Delta-E distance calculations ensuring no accessibility violations between series. Google's Material Design 3 introduced HCT (Hue-Chroma-Tone), a proprietary perceptually uniform space, though notably M3 does **not** provide a dedicated data visualization palette — a gap that highlights how specialized data viz color work has become.
The competitive gap for GitLab is clear: Pajamas needs to expand from 5 to **8–10 categorical hues**, adopt OKLCH for palette generation, implement a proper three-tier token architecture, define interactive states formally, and add pattern fill support as a toggleable accessibility layer.
| Metric | GitLab Pajamas | IBM Carbon | Atlassian | GitHub Primer |
|--------|---------------|------------|-----------|---------------|
| Categorical hues | 5 | 14 | 8 (tokens) | 17 + muted |
| Lightness steps per hue | 11 | 10 | 3 emphasis levels | Mark + muted |
| Total data viz tokens | 55 | ~160+ | 94 | ~40+ |
| Color space | Hex (hand-tuned) | Hex (perceptual) | Tokens | Hex (CSS vars) |
| Sequential palettes | Single-hue ramps | 4 hues, 10 steps | Via custom tokens | Not documented |
| Diverging palettes | Two-hue from center | 2 palettes, 16 steps | Via custom tokens | Not documented |
| Dark mode strategy | Step-based (500 pivot) | 4 themes, step remapping | Token aliasing | 6+ themes incl. CVD |
| Pattern fills | Not supported | Documented option | Not documented | Stroke styles mandated |
| Interactive states | Popover on hover | Full state tokens | Hover tokens | Not documented |
| CVD themes | Green+magenta status | Sequenced for CVD | CVD-tested | Tritanopia/colorblind themes |
## OKLCH should replace hand-tuned hex as the palette foundation
The single most impactful modernization is shifting palette generation to **OKLCH** (Lightness, Chroma, Hue), which is now natively supported in CSS Color Level 4 across all major browsers. Unlike HSL — where "50% lightness" produces wildly different perceived brightness for yellow versus blue — OKLCH guarantees that **equal L values produce equal perceived brightness across all hues**. This directly solves the core challenge of data visualization palettes: ensuring no single color dominates visual attention.
For categorical palettes, the technique is straightforward: fix L (lightness) and C (chroma), then rotate H in equal steps. For a **light-mode categorical palette** targeting 3:1 contrast on white (#FFFFFF), an L value of approximately **0.55–0.62** with C of **0.14–0.17** produces colors that are vibrant yet accessible. For dark-mode equivalents on a near-black surface (#18171D), target **L ≈ 0.72–0.78** with C reduced to **0.10–0.13** (desaturated ~20% to prevent optical vibration on dark backgrounds).
Here is a proposed **8-hue categorical palette** generated in OKLCH, with hex equivalents verified for 3:1+ contrast on both white and dark surfaces at the 500 pivot step:
| Hue Name | OKLCH (light) | Hex (light) | OKLCH (dark) | Hex (dark) | Hue° |
|----------|--------------|-------------|-------------|------------|------|
| Blue | oklch(0.55 0.16 250) | ~#1A6FD2 | oklch(0.75 0.12 250) | ~#6AAAF0 | 250 |
| Orange | oklch(0.60 0.16 70) | ~#B86E00 | oklch(0.78 0.12 70) | ~#E0A84D | 70 |
| Teal | oklch(0.58 0.12 185) | ~#007E7A | oklch(0.76 0.10 185) | ~#4DB8B4 | 185 |
| Magenta | oklch(0.55 0.17 350) | ~#C0347A | oklch(0.75 0.13 350) | ~#E882B4 | 350 |
| Purple | oklch(0.50 0.17 295) | ~#7B3FBF | oklch(0.72 0.13 295) | ~#B182E0 | 295 |
| Green | oklch(0.58 0.15 145) | ~#3A8A2A | oklch(0.76 0.12 145) | ~#6FC05E | 145 |
| Amber | oklch(0.65 0.15 90) | ~#9A8200 | oklch(0.80 0.11 90) | ~#C4B74D | 90 |
| Rose | oklch(0.58 0.16 15) | ~#C74E3A | oklch(0.76 0.12 15) | ~#E89482 | 15 |
These values should be hand-refined after generation — Stripe and Carbon both confirm that **computationally generated palettes need manual fine-tuning** for brand fit and edge-case legibility. Adobe Leonardo (leonardocolor.io) is the best tool for this workflow: it supports OKLCH interpolation, built-in contrast-ratio targeting, CVD simulation, and export to CSS, design tokens, and Tableau XML. Additional tools worth integrating into the workflow include Atmos (atmos.style) for curve-based OKLCH scale generation and ColorJS (colorjs.io) for programmatic gamut mapping.
For **sequential scales**, OKLCH excels: keep H constant, set C to a moderate value (0.08–0.14), and linearly interpolate L from ~0.95 (lightest) to ~0.30 (darkest) across 11 steps. This produces perceptually even ramps that degrade gracefully to grayscale — something hand-tuned hex values cannot guarantee.
## Accessibility requires more than 3:1 contrast ratios
The **3:1 contrast ratio for graphical objects** (WCAG 2.2 SC 1.4.11) remains the enforceable standard through 2026 and GitLab correctly targets this. However, three developments demand attention.
First, **APCA** (Advanced Perceptual Contrast Algorithm) is the proposed replacement for WCAG 2.x contrast math in the eventual WCAG 3.0. While APCA was removed from the WCAG 3 working draft in 2023 and remains far from finalization, it offers substantially better evaluation of dark-mode readability. APCA uses Lc (Lightness Contrast) values on a scale of roughly −108 to +105, and is polarity-sensitive — dark text on light backgrounds yields different values than the reverse. For data viz, target **Lc ≥ 45** for non-text graphical elements (roughly equivalent to the old 3:1). IBM Carbon internally targets **3.5:1** as a safety margin above the 3:1 minimum, a practice GitLab should adopt.
Second, colorblind-safe palettes should draw from established, rigorously tested sources. The **Okabe-Ito palette** (Bang Wong, Nature Methods 2011) remains the gold standard for ≤8 categorical colors: `#E69F00` (Orange), `#56B4E9` (Sky Blue), `#009E73` (Bluish Green), `#F0E442` (Yellow), `#0072B2` (Blue), `#D55E00` (Vermillion), `#CC79A7` (Reddish Purple), `#000000` (Black). **Paul Tol's Bright scheme** provides an excellent 7-color alternative: `#4477AA`, `#EE6677`, `#228833`, `#CCBB44`, `#66CCEE`, `#AA3377`, `#BBBBBB`. GitLab's green+magenta status pair is a sound choice — **blue + orange is the single safest 2-color combination** across all CVD types, and should be the default diverging endpoint pair.
Third, **color alone must never be the sole encoding**. GitHub Primer mandates different stroke styles (solid, dashed, dotted) per line series, different shape markers (circle, square, triangle) per data point, and limits charts to ≤5 lines or ≤5 pie slices. This level of prescriptive guidance is missing from Pajamas and should be added.
## Dark mode needs dedicated palettes, not just a 500-step pivot
GitLab's current approach — where step 500 serves as the pivot point working on both light and dark surfaces — is pragmatic but insufficient for optimal dark-mode data visualization. The industry has converged on **maintaining the same hue families but remapping to different lightness steps per theme**, with dark-mode palettes receiving approximately **20% less saturation** to prevent optical vibration on dark backgrounds.
Carbon uses the same 10-step scales but reverses the lightness direction: in light themes, the darkest color denotes the largest value; **in dark themes, the lightest color denotes the largest value**. Atlassian's token system maps the same semantic token name (`color.chart.categorical.1`) to entirely different primitive values per theme. Both approaches are superior to a single-pivot-step model because they optimize each palette independently for its surface.
The recommended approach for GitLab combines OKLCH-based generation with token aliasing:
- **Light mode**: Generate categorical colors at L ≈ 0.55–0.62, C ≈ 0.14–0.17 against white (#FFFFFF)
- **Dark mode**: Generate at L ≈ 0.72–0.78, C ≈ 0.10–0.13 against dark (#18171D)
- **Background surfaces**: Avoid pure black (#000000); use soft dark grays (#18171D or similar) to reduce halation
- **Gridlines/axes in dark mode**: Use subtle mid-grays (#333333–#3E3E3E), potentially dotted
- **Sequential palettes**: Reverse the lightness direction in dark mode (lightest = highest value)
- **Diverging palettes**: Carbon's finding that these can remain identical across themes is worth testing, as it reduces maintenance burden
CSS-native OKLCH enables an elegant implementation where only lightness and chroma parameters change per theme:
```css
:root {
--dataviz-L: 0.58;
--dataviz-C: 0.15;
}
[data-theme="dark"] {
--dataviz-L: 0.75;
--dataviz-C: 0.11;
}
--dataviz-cat-1: oklch(var(--dataviz-L) var(--dataviz-C) 250);
```
## Scaling beyond 8 categories demands strategy, not just more colors
Research consistently shows that **6–8 colors is the practical maximum** for categorical distinction in charts, with 10 as an absolute ceiling. Claus Wilke's widely-cited guidance states that beyond 8 categories, "the task of matching colors to categories becomes too burdensome." A 2026 CHI paper by Tseng et al. confirms that **redundant encoding** (color + shape together) significantly improves accuracy for 5–8 categories, with 88% accuracy versus 66% with color alone.
GitLab's current 5-hue system is too restrictive for complex dashboards. The recommended tiered strategy:
- **2–5 categories**: Use the first 5 categorical colors. This is the sweet spot.
- **6–8 categories**: Use the full 8-hue palette with direct labeling (not legends). Add redundant shape encoding for scatter plots.
- **9–12 categories**: Highlight 3–5 key series in color; render the rest in **neutral gray at opacity 0.3**. Provide interactive hover-to-highlight. Group remaining into "Other."
- **13+ categories**: Switch to small multiples (Tufte's principle). Use interactive filtering to let users select visible series. Provide a data table alternative.
**Pattern fills should be added as an optional, toggleable accessibility layer** — not a default. Apache ECharts (which GitLab already uses) ships a built-in `aria.decal` system: setting `aria.enabled: true` and `aria.decal.show: true` automatically applies distinguishable patterns to all series. This is the lowest-effort path for GitLab. Effective patterns include diagonal lines, dots, crosshatch, and horizontal/vertical lines. Patterns are best for **bar, column, and pie/donut charts** with large fill areas; for line charts, use dash patterns (solid, dashed, dotted, dash-dot) and point shape markers instead.
Important caveats: patterns can trigger photosensitivity in some users (avoid >5 repeating light-dark stripe pairs) and increase cognitive load. They should be user-activated, not default, aligning with GitLab's current caution about cognitive accessibility concerns.
## A three-tier token architecture for data visualization colors
The industry has converged on a **primitive → semantic → component** token model, and GitLab's existing constant → semantic → contextual framework maps directly to this pattern. The key recommendation is to create a dedicated `dataviz` token namespace separate from UI color tokens.
**Tier 1 — Primitive tokens** (not used directly in designs):
```
color.blue.100 = #E8F0FE
color.blue.500 = #1A6FD2
color.blue.900 = #0A2E5C
color.teal.500 = #007E7A
...
```
**Tier 2 — Semantic tokens** (the primary tokens designers and developers use):
```
// Categorical (abstract, index-based naming)
color.dataviz.categorical.1 → color.teal.700 (light) / color.teal.400 (dark)
color.dataviz.categorical.2 → color.purple.500 (light) / color.purple.400 (dark)
color.dataviz.categorical.1.hovered → color.teal.800 (light) / color.teal.300 (dark)
color.dataviz.categorical.1.muted → color.teal.700 at 20% opacity
// Sequential
color.dataviz.sequential.blue.100 → lightest step
color.dataviz.sequential.blue.900 → darkest step
// Diverging
color.dataviz.diverging.negative.500 → magenta endpoint
color.dataviz.diverging.neutral → midpoint gray
color.dataviz.diverging.positive.500 → green endpoint
// Status (semantic naming is correct here)
color.dataviz.status.success → green
color.dataviz.status.warning → amber
color.dataviz.status.danger → magenta/vermillion
color.dataviz.status.info → blue
// Structural
color.dataviz.axis → neutral gray
color.dataviz.grid → subtle gray
color.dataviz.background → surface color
```
**Tier 3 — Component tokens** (optional, for chart-component-specific overrides):
```
bar-chart.bar.fill.1 → color.dataviz.categorical.1
line-chart.line.stroke.1 → color.dataviz.categorical.1
pie-chart.slice.fill.1 → color.dataviz.categorical.1
chart.tooltip.background → color.dataviz.background
```
Critical naming decisions: **use abstract index-based names** (`categorical.1`, `categorical.2`) for categorical tokens — not hue names like `dataviz.blue`. This enables theme switching and rebranding without renaming tokens. Reserve hue-specific names for custom/user-selectable chart colors (e.g., `color.dataviz.custom.blue`). Atlassian, Carbon, and Cloudscape all follow this pattern.
The tokens should be published in **W3C DTCG format** (the first stable version shipped October 2025) using `.tokens.json` files with `{curly.brace}` aliasing syntax, and processed through Style Dictionary v4 for cross-platform output.
## Interactive states need formal definitions with specific values
GitLab's current interactive guidance is limited to "a popover on hover." Based on Carbon, Highcharts, and D3.js patterns, the complete state map should be:
| State | Visual treatment | Value | Transition |
|-------|-----------------|-------|------------|
| **Default** | Full opacity, base color, no stroke | opacity: 1.0 | — |
| **Hover (element)** | Half-step lighter/darker in OKLCH (L ± 0.05) | opacity: 1.0 | 150ms ease-out |
| **Hover (non-hovered)** | Dim all other series | **opacity: 0.2** | 250ms ease-out |
| **Hover (halo)** | Semi-transparent ring around data point | opacity: 0.25 | 100ms ease-out |
| **Selected** | Two lightness steps darker (light) or lighter (dark) | opacity: 1.0, 2px accent border | 70ms |
| **Focus** | 2px outline, Blue 60 (#0f62fe) light / White dark | ≥3:1 contrast, `:focus-visible` only | instant |
| **Muted** | Applied when another element is actively selected | opacity: 0.2, no pointer events | 250ms ease-out |
| **Disabled** | Grayscale, reduced opacity | opacity: 0.3–0.4, cursor: not-allowed | — |
The **0.2 opacity for muted/inactive series** is the most consistent value across implementations (Highcharts default, D3.js community standard). For keyboard navigation, charts should be a single tab stop with arrow keys navigating between data points within the chart. SVG elements need `tabindex="0"`, `role="img"`, and `aria-label` attributes. The `aria-activedescendant` pattern manages focus within the chart widget.
For animation timing, Carbon's token system provides good reference values: **70ms** for micro-interactions (button presses), **150ms** for hover color transitions, **250ms** for opacity fades, **400ms** for chart entrance animations. All transitions should respect `prefers-reduced-motion: reduce` by removing position/scale animations (color transitions are generally acceptable even under reduced motion).
## Status colors should pair green+magenta with mandatory non-color encoding
GitLab's choice of **green + magenta** instead of green + red for success/failure is a defensible, colorblind-conscious decision — magenta is distinguishable from green across deuteranopia and protanopia. However, the recommendation should be strengthened:
The **safest universal status pair** is **blue + orange**, which is distinguishable by virtually all forms of color vision deficiency. For a multi-level status system, the recommended values (drawing from Carbon's alert palette and Okabe-Ito research):
| Status | Light hex | Dark hex | OKLCH (light) | Mandatory non-color indicator |
|--------|-----------|----------|---------------|-------------------------------|
| Success | #198038 | #42BE65 | oklch(0.52 0.14 150) | Checkmark icon ✓ + text label |
| Warning | #B28600 | #F1C21B | oklch(0.65 0.15 85) | Triangle icon △ + text label |
| Danger | #C0347A | #E882B4 | oklch(0.55 0.17 350) | X icon ✕ + text label |
| Info | #0F62FE | #4589FF | oklch(0.50 0.18 260) | Circle icon ⓘ + text label |
Carbon mandates that status indicators use **at least 2 of: color, shape, and symbol**. Each status should have a distinct icon shape — never the same shape in different colors within one experience. When encoding status in charts specifically, combine the colored fill with either pattern fills (for area charts), distinct point shapes (for scatter/line), or direct text annotations. GitLab's existing stance that "pattern fills can create accessibility issues of their own, namely cognitive" is valid — the solution is making patterns an **opt-in toggle**, not removing them as an option entirely.
## Concrete modernization roadmap for Pajamas
Based on this analysis, the recommended implementation sequence prioritizes high-impact, low-risk changes first:
**Phase 1 — Foundation (token architecture + OKLCH generation)**
Restructure existing 55 color variables into the three-tier token model. Generate all scales in OKLCH using Adobe Leonardo, then hand-tune. Expand from 5 to 8 categorical hues. Define the semantic token naming convention (`color.dataviz.categorical.N`). Publish in W3C DTCG format. This phase does not change visual output — it restructures the system for everything that follows.
**Phase 2 — Accessibility hardening (interactive states + patterns + encoding)**
Define the complete interactive state map (hover, selected, muted, focus, disabled) with specific OKLCH transformations and opacity values. Enable Apache ECharts' `aria.decal` system as a toggleable pattern-fill option. Document mandatory redundant encoding rules: stroke styles for line charts, shape markers for scatter plots, direct labeling preference over legends, maximum series limits per chart type (≤5 lines, ≤5 pie slices, ≤8 categorical colors before grouping).
**Phase 3 — Theme optimization (dark mode + high contrast)**
Create dedicated dark-mode primitive palettes with reduced chroma. Implement OKLCH-based lightness/chroma swapping via CSS custom properties. Test with APCA in addition to WCAG 2.2 ratios. Consider following Primer's lead in offering dedicated CVD themes (tritanopia, colorblind) as an advanced accessibility feature — this would be a genuine differentiator in the DevOps tool space.
**Phase 4 — Documentation and tooling**
Build the chart examples currently marked TODO. Create a sequence generator (also marked TODO) using OKLCH rotation. Publish a Figma plugin or variable collection for the new token system. Document the scalability strategy (what to do at 5, 8, 12, 20+ categories). Provide a CVD simulation checklist integrated into the design review process.
## Conclusion
GitLab's Pajamas data visualization color system has a solid accessibility-first foundation with its 3:1 contrast targeting and green+magenta status pair. The gaps — limited to 5 hues, no formal token architecture, missing interactive states, no pattern fills, hand-tuned hex values rather than perceptually uniform generation — are all addressable without breaking existing implementations. **The single highest-value change is adopting OKLCH as the generative color space**, because it cascades into better sequential scales, more predictable dark-mode transformations, consistent perceptual weight across hues, and simpler maintenance. The second-highest is implementing the three-tier token architecture, which enables everything from theming to white-labeling to CVD-specific modes. Combined with the interactive state definitions and Apache ECharts' built-in pattern support that GitLab is already positioned to use, these changes would move Pajamas from mid-pack to genuinely competitive with Carbon and Primer — the current industry benchmarks for data visualization color systems.
issue