Color.Palette.Contrast
(Color v0.11.0)
Copy Markdown
A contrast-targeted palette — shades of a seed colour chosen so each stop hits a specific contrast ratio against a fixed background.
This is the algorithm behind Adobe Leonardo.
Unlike Color.Palette.Tonal, which produces visually even
lightness steps, this palette produces contrast-wise even
steps. Each stop in the result satisfies the WCAG or APCA
contrast ratio you asked for — exactly — against the background
you specified.
Use it when you need:
Component states (resting, hover, active, focus, disabled) that all meet a specific contrast requirement.
Text shades guaranteed to pass AA (4.5:1) or AAA (7:1) on a known surface.
APCA-driven text hierarchies, with
:metric: :apcaand targets in the 60–90 Lc range.
Algorithm
Convert the seed to Oklch. Keep its hue and chroma.
For each target contrast ratio, binary search over Oklch lightness
L ∈ [0, 1]for a colour whose contrast against the background matches the target. Contrast is monotonic in lightness (for a fixed hue / chroma), so binary search converges in ~20 iterations to sub-0.01 precision.Gamut-map each candidate into sRGB with
Color.Gamut.If the search direction (lighter vs darker than background) can't reach the target, the stop is marked
:unreachable. This happens when the target ratio is higher than what the seed's hue and chroma can achieve against the given background.
Metrics
:wcag(default) — WCAG 2.x contrast ratio, viaColor.Contrast.wcag_ratio/2. Targets typically in[1.0, 21.0].:apca— APCA W3 0.1.9 Lc value, viaColor.Contrast.apca/2. Targets typically in[15, 108](absolute value).
Summary
Functions
Fetches the colour for a given target contrast from a palette.
Returns a detailed gamut report on a contrast palette.
Returns true when every reachable stop in the palette is
inside the given RGB working space.
Generates a contrast-targeted palette.
Emits the palette as a W3C Design Tokens Community Group color-token group.
Types
@type stop() :: %{ target: number(), achieved: number(), color: Color.SRGB.t() | :unreachable }
@type t() :: %Color.Palette.Contrast{ background: Color.SRGB.t(), metric: :wcag | :apca, name: binary() | nil, options: keyword(), seed: Color.SRGB.t(), stops: [stop()] }
Functions
@spec fetch(t(), number()) :: {:ok, Color.SRGB.t()} | :error
Fetches the colour for a given target contrast from a palette.
If the target was not in the original :targets list, or no
lightness was found that satisfied it, returns :error.
Arguments
paletteis aColor.Palette.Contraststruct.targetis the contrast target to look up.
Returns
{:ok, color}on success,:errorotherwise.
Examples
iex> palette = Color.Palette.Contrast.new("#3b82f6", targets: [4.5, 7.0])
iex> {:ok, _color} = Color.Palette.Contrast.fetch(palette, 4.5)
iex> Color.Palette.Contrast.fetch(palette, 99.0)
:error
@spec gamut_report(t(), Color.Types.working_space()) :: map()
Returns a detailed gamut report on a contrast palette.
Each reachable stop becomes a %{target, color, in_gamut?}
entry; unreachable stops become %{target, color: :unreachable}.
Arguments
paletteis aColor.Palette.Contraststruct.working_spaceis an RGB working-space atom. Defaults to:SRGB.
Returns
- A map with
:working_space,:in_gamut?,:stops, and:out_of_gamut.
Examples
iex> palette = Color.Palette.Contrast.new("#3b82f6", targets: [4.5, 7.0])
iex> %{in_gamut?: true} = Color.Palette.Contrast.gamut_report(palette, :SRGB)
@spec in_gamut?(t(), Color.Types.working_space()) :: boolean()
Returns true when every reachable stop in the palette is
inside the given RGB working space.
Unreachable stops (:unreachable sentinel) are ignored — they
have no colour to check.
Arguments
paletteis aColor.Palette.Contraststruct.working_spaceis an RGB working-space atom. Defaults to:SRGB.
Returns
- A boolean.
Examples
iex> palette = Color.Palette.Contrast.new("#3b82f6", targets: [4.5, 7.0])
iex> Color.Palette.Contrast.in_gamut?(palette, :SRGB)
true
@spec new( Color.input(), keyword() ) :: t()
Generates a contrast-targeted palette.
Arguments
seedis anything accepted byColor.new/1. Its hue and chroma are preserved; lightness is swept to hit each target.
Options
:backgroundis the colour to measure contrast against. Defaults to"white".:targetsis a list of target contrast values. Defaults to[1.25, 1.5, 2.0, 3.0, 4.5, 7.0, 10.0, 15.0]for WCAG or[15, 30, 45, 60, 75, 90]for APCA.:metricis:wcag(default) or:apca.:gamutis the working space to gamut-map each stop into, default:SRGB.:nameis an optional string label.
Returns
- A
Color.Palette.Contraststruct. Each entry in:stopshas:target,:achieved, and:colorfields.:coloris:unreachableif no lightness produces the requested contrast.
Examples
iex> palette = Color.Palette.Contrast.new("#3b82f6", targets: [3.0, 4.5, 7.0])
iex> length(palette.stops)
3
iex> Enum.all?(palette.stops, & &1.color != :unreachable)
true
Emits the palette as a W3C Design Tokens Community Group color-token group.
Each reachable stop becomes a DTCG color token keyed by the
target contrast value (integer if it rounds to one, otherwise
decimal). Unreachable stops are emitted as tokens whose
$value is null with an $extensions.color.reason field
explaining the exclusion — tools that filter on $value
presence will skip them cleanly.
Arguments
paletteis aColor.Palette.Contraststruct.
Options
:spaceis the colour space for emitted stop values. Any module accepted byColor.convert/2. DefaultColor.Oklch.:nameoverrides the group name. Defaults to the palette's:namefield, or"contrast"if unset.
Returns
- A map shaped as
%{"<name>" => %{"<target>" => token, ...}}.
Examples
iex> palette = Color.Palette.Contrast.new("#3b82f6", targets: [4.5, 7.0])
iex> tokens = Color.Palette.Contrast.to_tokens(palette)
iex> tokens["contrast"]["4.5"]["$type"]
"color"