Dialtone color tokens and CSS have moved from HSL to OKLCH. Per-channel breakout variables are removed, cutting CSS bundle over 30%, and removing over 3,200 CSS Variables. Migration scripts included.
--dt-color-*-h, -s, -l, -a, -hsl, -hsla) are removed.~"" string escaping around color functions is no longer needed.--dt-color-*-h, -s, -l, -hsl, -hsla. These variables no longer exist.OKLCH (oklch(L C H)) is a perceptually uniform color space where L = lightness (0–1), C = chroma (0+), and H = hue (0–360). It replaces hsl()/hsla() across Dialtone for three reasons:
Perceptual uniformity. Equal numeric changes produce equal visual changes. In HSL, hsl(60, 100%, 50%) (yellow) and hsl(240, 100%, 50%) (blue) have the same "lightness" but vastly different perceived brightness. OKLCH corrects this, making lightness adjustments predictable across all hues.
Better gradients. Gradients interpolated in OKLCH avoid the muddy grays that appear in HSL transitions between distant hues. Colors stay vivid across the gradient.
Modern CSS integration. OKLCH enables relative color syntax (oklch(from var(--token) l c h / alpha)), which can decompose any color into its channels at render time. This eliminates the need for pre-computed breakout variables, directly enabling the bundle size reduction below.
This looks to be the single most impactful performance benefit. Every CSS custom property declaration the browser loads must be:
Custom properties are especially expensive in the cascade because they participate in inheritance at every node. Removing ~3,200 declarations per theme speeds up style recalculation on every interaction, not just the initial page load.
Removing the per-channel breakout variables eliminated ~3,200 CSS custom property declarations per theme. These variables (-h, -s, -l, -a, -hsl, -hsla for each color token) were the single largest contributor to Dialtone's CSS bundle size.
The file that matters most is dialtone-default-theme.min.css, which every Dialpad product loads:
| File | Before | After | Reduction |
|---|---|---|---|
dialtone-default-theme.min.css | 1,238 KB | 853 KB | -31.1% |
dialtone-default-theme.min.css (gzip) | 132 KB | 95 KB | -27.7% |
tokens-dp-light.css | 370 KB | 105 KB | -71.5% |
tokens-base-light.css | 99 KB | 31 KB | -68.8% |
That's 385 KB fewer on the theme file that every user loads. The token CSS files, dominated by breakout declarations, saw 68–72% reductions. Products that load token files independently (theming scenarios) benefit the most.
Beyond performance, the migration simplifies how developers work with Dialtone colors:
calc(lightness - 5%) darkens yellow and purple by different perceived amounts. OKLCH lightness is perceptually uniform, so calc(l - 0.05) produces a consistent visual shift across all hues.oklch(from var(--token) l c h / 0.5) reads as "this token at 50% opacity." Compare that to hsla(var(--token-h), var(--token-s), var(--token-l), 0.5).~"" escaping. LESS parses hsl() as a built-in function, which breaks when CSS custom properties are used inside it. oklch() passes through LESS untouched, eliminating an entire class of compilation bugs.If your code references any HSL channel breakout variables, it needs to be updated. Refer to the HSL to OKLCH Migration Tool for help. Here are some common patterns and their replacements:
Before: composite HSL var + alpha
color: hsla(var(--dt-color-foreground-primary-hsl) / 0.5);
After: relative color syntax
color: oklch(from var(--dt-color-foreground-primary) l c h / 0.5);
Before: separate H,S,L vars + alpha
color: hsla(
var(--dt-color-foreground-primary-h),
var(--dt-color-foreground-primary-s),
var(--dt-color-foreground-primary-l),
0.25
);
After: single token reference
color: oklch(from var(--dt-color-foreground-primary) l c h / 0.25);
Before: HSL var without alpha
color: hsl(var(--dt-color-foreground-primary-hsl));
After: direct token reference
color: var(--dt-color-foreground-primary);
Before: LESS ~"" escape
color: ~"hsla(var(--dt-color-black-900-hsl) / 0.5)";
After: no escaping necessary
color: oklch(from var(--dt-color-black-900) l c h / 0.5);
LESS required ~"" string escaping because it tried to parse hsl() as a built-in function. LESS does not recognize oklch(), so the wrapper is no longer necessary.
All internal Dialtone CSS has already been converted. When writing new styles:
var(--dt-color-foreground-primary), never channel suffixes.oklch(from var(--token) l c h / 0.5).oklch(from var(--token) calc(l - 0.05) c h). OKLCH lightness is 0–1 (not 0–100% like HSL), so -5% becomes -0.05.oklch(from var(--token) l 0 h).oklch(from var(--token) calc(l - 0.05) c h / 0.5).Dialtone's opacity utilities (d-fco*, d-bgo*, d-bco*, d-dco*) continue to work exactly as before. The underlying mechanism uses CSS relative color syntax with an alpha keyword fallback:
/* Generated utility — alpha keyword preserves the token's original opacity */
.d-fc-primary {
color: oklch(from var(--dt-color-foreground-primary) l c h / var(--fco, alpha));
}
/* Opacity utility overrides the alpha channel */
.d-fco50 { --fco: 50%; }
When no opacity utility is applied, var(--fco, alpha) falls back to the alpha keyword, preserving the source color's original alpha (e.g. 0.65 for muted colors). When an opacity utility like .d-fco50 is applied, it sets --fco to 50%, overriding the alpha channel.
For custom CSS, apply alpha using relative color syntax:
/* Hardcoded alpha */
background: oklch(from var(--dt-color-surface-primary) l c h / 0.1);
/* Dynamic alpha via CSS variable */
background: oklch(from var(--dt-color-surface-primary) l c h / var(--my-alpha, 1));
Relative color syntax lets you adjust any channel of a color token — lightness, chroma (saturation), and hue:
/* Darken by reducing lightness */
color: oklch(from var(--dt-color-foreground-critical) calc(l - 0.05) c h);
/* Lighten */
color: oklch(from var(--dt-color-foreground-critical) calc(l + 0.1) c h);
/* Desaturate (remove chroma) */
color: oklch(from var(--dt-color-foreground-critical) l 0 h);
/* Shift hue */
color: oklch(from var(--dt-color-foreground-critical) l c calc(h + 30));
/* Combine: darken + reduce opacity */
color: oklch(from var(--dt-color-foreground-critical) calc(l - 0.05) c h / 0.5);
dialtone-migration-helper includes an hsl-to-oklch config that automates ~90% of consumer HSL breakout usage conversions.
The tool migrates:
-hsl/-hsla var patterns (with and without alpha)-h, -s, -l channel var patterns (comma and space syntax)calc() on lightness channel~"" string escaping (stripped automatically)Requires manual review:
@step0, @step1) — app-internal, outside Dialtone's scopehsla(137, 100%, 27%, 0.05)) — should adopt Dialtone tokenscalc() on lightness: OKLCH lightness is perceptually different from HSL, so visually verify adjusted valuesnpx dialtone-migration-helper --cwd ./src
# Select "hsl-to-oklch" from the config list
npx dialtone-migration-helper --cwd ./src --force
# Select "hsl-to-oklch" from the config list
Migrating Colors from HSL to OKLCH documentation last updated Friday, September 4, 2026
fix/popover-modal-zindex-scope