Skip to content
Glazev3.1.0
Color scheme
Contrast
Color scheme
Contrast

Mixes and transparency

A mix color blends a base color toward a target color by value, from 0 (pure base) to 100 (pure target). An opaque mix produces a solid color; a transparent mix produces the target with an opacity, for the browser to composite over the base. Both read base and target per scheme, so they adapt to light, dark, and high contrast automatically.

import { glaze } from '@tenphi/glaze';

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 95 },
  accent: { tone: 30 },

  // A solid tint: 30% of the way from surface to accent.
  'accent-tint': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 30,
  },

  // A translucent wash: accent at 8% opacity, composited over surface.
  'accent-hover': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 8,
    blend: 'transparent',
  },
});

Every field is listed in Color definitions.

Opaque mix

An opaque mix (the default, blend: 'opaque') interpolates between the resolved base and target and emits a solid color with alpha 1. value: 0 is the base, value: 100 is the target. Because base and target are resolved for each scheme first, the mix follows them: a tint of the accent over the surface in light mode is a tint of the dark-mode accent over the dark-mode surface in dark mode.

Blend space

space picks where an opaque mix interpolates:

space Behavior Best for
'okhsl' (default) Perceptually uniform OKHSL interpolation: hue, saturation, and lightness each move evenly. Design token derivation.
'srgb' Linear-light sRGB channel interpolation, like CSS color-mix(in srgb-linear, …). Physical light mixing.

The same two colors, mixed at every tenth of the way in each space:

space: 'okhsl'
040
1046
2051
3057
4062
5067
6072
7077
8081
9086
10090
space: 'srgb'
040
1050
2058
3064
4069
5074
6078
7081
8084
9087
10090
Blue (hue 260, tone 40) toward yellow (hue 95, tone 90). Top number: value. Bottom number: the result's tone. OKHSL walks the hue around the wheel through cyan and green; linear sRGB passes through desaturated colors.
space: 'okhsl'
00
105
2014
3026
4039
5051
6062
7073
8083
9092
100100
space: 'srgb'
00
1036
2053
3064
4072
5079
6084
7089
8093
9097
100100
Black toward white. OKHSL spaces lightness evenly; linear light gets bright quickly, so most of the ramp sits at high tones.

Achromatic colors

When one side of an okhsl mix has a saturation of zero (a color defined with saturation: 0, such as a pure white, black, or gray), its hue carries no information, so the mix takes the hue of the color that has saturation. This matches the “missing component” behavior of CSS color-mix(): a mix from such a white toward an accent stays in the accent’s hue the whole way instead of sweeping through other hues.

Only zero counts, along with float noise up to 1e-6. A gray given as a value, such as '#f5f5f5' or rgb(250 250 250), parses with a saturation of zero, and so does an srgb mix that lands on a gray. A white or black that keeps real saturation, such as { tone: 100 } in a saturated theme, still carries its hue, and a mix toward an accent of another hue sweeps between the two. A mix between two zero-saturation colors stays gray in either space; the spaces differ only in how lightness is spread along the ramp (see black toward white above).

space: 'okhsl'
0100
1095
2090
3085
4079
5073
6067
7061
8054
9047
10040
White toward a hue-280 accent in OKHSL. White has no hue, so every step uses the accent's hue while saturation and lightness move.

Transparent mix

With blend: 'transparent', the output is the target color (its resolved hue, saturation, and tone, in the mix’s gamut when their pastel flags differ) with alpha = value / 100. Nothing is blended in Glaze; the browser composites it over whatever is underneath, which should be the base.

theme.colors({
  surface: { tone: 95 },
  black: { tone: 0, saturation: 0 },
  hover: {
    type: 'mix',
    base: 'surface',
    target: 'black',
    value: 8,
    blend: 'transparent',
  },
});
// hover → the resolved `black` with alpha 0.08

The target is resolved like any color first. Here black uses the default mode: 'auto', so it inverts to a near-white in dark schemes and the same hover lightens the dark surface instead of darkening it.

space is ignored for transparent mixes. Browsers composite source-over on gamma-encoded sRGB: 50% black over white paints #808080, not the #bcbcbc linear light would give. Glaze’s contrast solver for transparent mixes models exactly that, so a solved opacity renders at the contrast it was solved for.

theme.ts

ts

import { glaze } from '@tenphi/glaze';

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 97 },
  accent: { tone: 45 },
  'wash-sm': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 6,
    blend: 'transparent',
  },
  'wash-md': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 15,
    blend: 'transparent',
  },
  'wash-lg': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 30,
    blend: 'transparent',
  },
});
surface
−2.5+2.4−2.6+1.7wash-smα 0.0600.0600.0600.060
−6.4+6.2−6.7+5.0wash-mdα 0.1500.1500.1500.150
−13.3+13.0−13.9+12.1wash-lgα 0.3000.3000.3000.300
Accent washes over the surface in your current scheme. The big number is the tone step the composite lands; the opacity is value / 100 in every scheme, so the step changes when the surface does.

A transparent mix keeps the opacity you author. That makes it the right tool for a brand-colored wash, but it does not land the same visible step on every surface. For an overlay that does, use a shadow or glow, whose opacity is solved per surface.

Contrast on mixes

Mixes take the same contrast field as regular colors, with a WCAG ratio or preset, or an APCA target (see Contrast solving). The floor is measured between the mix and its base. The solver keeps your value when it already meets the floor; otherwise it moves the mix ratio (opaque) or the opacity (transparent) toward the target until it does, staying as close to your value as it can.

theme.ts

ts

import { glaze } from '@tenphi/glaze';

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 95 },
  accent: { tone: 30 },
  'accent-border': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 20,
    contrast: 3,
  },
  'accent-wash': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: 4,
    blend: 'transparent',
    contrast: 1.25,
  },
});
accent-border
1.381.381.401.37:1as authored · value 20.020.020.020.0
3.033.033.033.03:1with contrast · value 61.764.059.158.4
accent-wash
1.061.071.061.05:1as authored · opacity 4.04.04.04.0
1.261.261.261.26:1with contrast · opacity 15.013.714.514.7
Each mix as authored (no floor) and with its floor, over the surface in your current scheme. The big number is the WCAG ratio against the surface; below it, the value or opacity the mix ended up with.
  • Both value and contrast accept [normal, highContrast] pairs, and value follows a manual contrast level like other pairs. Contrast presets escalate in high contrast as they do for regular colors (high contrast).
  • For APCA floors, role sets the polarity of the mix against its base, exactly as on a regular color (roles).
  • When even the pure target cannot meet the floor, the mix settles at the pure target for any value below 100.
  • A value of 100 is the pure target already. Glaze keeps it whenever it clears the floor, even inside the solver’s safety margin. When it misses, there is no room above, so the mix falls back to value: 0: the base itself, and a transparent mix vanishes. That includes a high-contrast entry of 100. Keep value below 100 when you set a floor.

An unmet floor warns once per scheme, like it does for a regular color. Below, accent-fill meets AA in light at about 4.55:1. In dark the pure target reaches only 4.24:1, so the mix settles there. High contrast escalates the floor to 7:1, which the pure target misses (5.72:1 in light high contrast, 5.20:1 in dark), so the high-contrast entry of 100 falls back to the surface:

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 95 },
  accent: { tone: 40 },
  'accent-fill': {
    type: 'mix',
    base: 'surface',
    target: 'accent',
    value: [80, 100],
    contrast: 'AA',
  },
});
glaze: mix "accent-fill" cannot meet WCAG 7.00 against its base in
  lightContrast scheme (got 1.00).
Even the pure target falls short; try lowering the contrast target or
  mixing toward a color further from the base.

The darkContrast warning is the same with its scheme name, and the dark-scheme one names WCAG 4.50 and (got 4.24) instead. The fix is a target further from the base or a lower floor. Here contrast: ['AA', 'AA'] holds high contrast at 4.5:1, which the pure target clears, so its entry of 100 is kept. See Warnings.

Mix chaining

A mix can use another mix as its base or target:

theme.ts

ts

import { glaze } from '@tenphi/glaze';

const theme = glaze(0, 0);

theme.colors({
  white: { tone: 100, saturation: 0 },
  black: { tone: 0, saturation: 0 },
  gray: {
    type: 'mix',
    base: 'white',
    target: 'black',
    value: 50,
    space: 'srgb',
  },
  'gray-light': {
    type: 'mix',
    base: 'white',
    target: 'gray',
    value: 50,
    space: 'srgb',
  },
});
whitetone 100.0
blacktone 4.9
graytone 79.0
gray-lighttone 91.2
Light scheme, where the auto black sits at the light tone window's floor rather than tone 0. gray is halfway from white to black in linear sRGB; gray-light is halfway from white to gray. In dark schemes white and black invert, so the whole chain mirrors.

A mix cannot reference a shadow or glow color, as base or as target; Glaze throws when the theme resolves. The same restriction applies to a regular color’s base.

Other translucent colors are accepted but lose their alpha. A transparent mix or a color with opacity, used as a mix’s base or target (or as a shadow’s bg), counts as its opaque color: a mix toward a 10% accent wash comes out the same as a mix toward the accent. Chain off opaque colors.

Pastel mixes

A mix’s pastel sets the gamut of the color it emits. Like any color, a mix takes the theme’s pastel unless it sets its own. Its base and target keep their own flags, and when a flag differs from the mix’s, the mix reads that color in its own gamut through the color it paints:

  • A standard mix holds every pastel color, so value: 0 paints the base and value: 100 the target exactly.
  • A pastel mix caps a vivid color at the pastel limit, keeping its hue and lightness. A pastel mix toward a vivid brand color ends at the brand with its saturation capped.
  • A transparent mix’s overlay is the target in the mix’s gamut, and an srgb mix interpolates the colors as painted, then caps the result.
const theme = glaze(280, 80, { pastel: true });

theme.colors({
  surface: { tone: 95 },
  brand: { tone: 40, pastel: false },
  // pastel like the theme: at value 100 it would be the brand, capped
  'brand-soft': { type: 'mix', base: 'surface', target: 'brand', value: 60 },
  // standard: the brand in full at value 100
  'brand-tint': {
    type: 'mix',
    base: 'surface',
    target: 'brand',
    value: 60,
    pastel: false,
  },
});

The contrast solver measures the color the mix paints, so a floor holds on screen. When a pastel mix misses its floor because the cap took the target’s saturation, the warning says so; pastel: false on the mix paints the target in full.

Mix, shadow, glow, or opacity?

You want Use
The same visible darkening or lightening on any surface: state layers, elevation, drop shadows shadow / glow
A solid color part of the way from one color to another Opaque mix
A translucent color over a known base, such as a brand-colored wash Transparent mix
A translucent color with a contrast floor against its base Transparent mix with contrast
One fixed alpha everywhere, such as a 50% scrim A regular color with opacity

Shadows and glows solve opacity for a tone step, transparent mixes keep (or solve, with contrast) an opacity against their base, and opacity is never solved.