Color.Palette (Color v0.11.0)

Copy Markdown

Palette generation for design systems and web sites.

This module is the public façade for several palette-generation algorithms. Each algorithm lives in its own submodule and returns a struct with the generated colours and the parameters that produced them.

Algorithms

  • tonal/2 — a single tonal scale (Tailwind / Radix / Open Color style) — one seed colour, N shades from light to dark, useful for bg-primary-50 through bg-primary-950 design tokens.

  • theme/2 — a complete Material Design 3 style theme from one seed: five coordinated tonal scales (primary, secondary, tertiary, neutral, neutral-variant), addressable by Material role names like :on_primary or :surface_variant.

  • contrast/2 — a contrast-targeted palette (Adobe Leonardo style) — shades that hit specific WCAG or APCA contrast ratios against a chosen background. Use this when you need accessibility-guaranteed component states.

  • contrast_scale/2 — a contrast-constrained tonal scale (Matt Ström-Awn's approach) — a numbered scale where any two stops ≥ apart label units apart are guaranteed to satisfy a minimum contrast ratio. A hybrid between tonal and contrast.

Working space

All palette algorithms operate in Oklch, the cylindrical variant of Oklab. Oklch is perceptually uniform for lightness, which is exactly what tonal scales need: equal lightness steps look like equal lightness steps to the eye. After generation, each stop is gamut-mapped to sRGB via Color.Gamut.to_gamut/3 using the CSS Color 4 algorithm so that no stop ever falls outside the displayable cube.

Summary

Functions

Generates a contrast-targeted palette — shades whose contrast against a chosen background matches a list of target ratios. See Color.Palette.Contrast for the full algorithm and option list.

Generates a contrast-constrained tonal scale. See Color.Palette.ContrastScale for the full algorithm.

Returns a detailed gamut report on the given palette.

Returns true if every stop in the given palette is inside the chosen RGB working space.

Generates a colour in the given category while preserving the seed's perceived lightness and chroma.

Returns the full list of category atoms accepted by semantic/3.

Generates a complete Material Design 3 style theme from a seed colour. See Color.Palette.Theme for the full algorithm and option list.

Generates a tonal scale — N shades of a single hue — from a seed colour. See Color.Palette.Tonal for the full algorithm.

Functions

contrast(seed, options \\ [])

@spec contrast(
  Color.input(),
  keyword()
) :: Color.Palette.Contrast.t()

Generates a contrast-targeted palette — shades whose contrast against a chosen background matches a list of target ratios. See Color.Palette.Contrast for the full algorithm and option list.

Arguments

Options

See Color.Palette.Contrast.new/2.

Returns

Examples

iex> palette = Color.Palette.contrast("#3b82f6", targets: [4.5, 7.0])
iex> length(palette.stops)
2

contrast_scale(seed, options \\ [])

@spec contrast_scale(
  Color.input(),
  keyword()
) :: Color.Palette.ContrastScale.t()

Generates a contrast-constrained tonal scale. See Color.Palette.ContrastScale for the full algorithm.

Arguments

Options

See Color.Palette.ContrastScale.new/2.

Returns

Examples

iex> palette = Color.Palette.contrast_scale("#3b82f6")
iex> Map.keys(palette.stops) |> Enum.sort()
[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]

gamut_report(palette, working_space \\ :SRGB)

@spec gamut_report(
  struct(),
  Color.Types.working_space()
) :: map()

Returns a detailed gamut report on the given palette.

Dispatches on palette type — see each palette module's gamut_report/2 for the returned map's exact shape.

Arguments

  • palette is any palette struct produced by this module.

  • working_space defaults to :SRGB.

Returns

  • A map. The top-level :in_gamut? key is present on every palette type.

Examples

iex> palette = Color.Palette.tonal("#3b82f6")
iex> report = Color.Palette.gamut_report(palette, :SRGB)
iex> report.in_gamut?
true

in_gamut?(palette, working_space \\ :SRGB)

@spec in_gamut?(
  struct(),
  Color.Types.working_space()
) :: boolean()

Returns true if every stop in the given palette is inside the chosen RGB working space.

Dispatches on the palette struct type, so works uniformly for Color.Palette.Tonal, Color.Palette.Theme, Color.Palette.Contrast, and Color.Palette.ContrastScale.

Intended primarily for CI checks — call once per palette and fail the build if the result is false.

Arguments

  • palette is any palette struct produced by this module.

  • working_space is an RGB working-space atom. Defaults to :SRGB.

Returns

  • A boolean.

Examples

iex> palette = Color.Palette.tonal("#3b82f6")
iex> Color.Palette.in_gamut?(palette)
true

iex> theme = Color.Palette.theme("#3b82f6")
iex> Color.Palette.in_gamut?(theme, :SRGB)
true

semantic(seed, category, options \\ [])

@spec semantic(Color.input(), atom(), keyword()) :: struct()

Generates a colour in the given category while preserving the seed's perceived lightness and chroma.

Useful for synthesising semantic colours — success, danger, warning, info — that feel like they belong to the same palette as a brand seed, without every brand needing hand-picked accents.

The algorithm is deliberately simple: convert the seed to Oklch, look up the category's canonical hue (e.g. red ≈ 25°, green ≈ 145°, blue ≈ 250°), build an Oklch colour at that hue with the seed's lightness and chroma, and gamut-map into sRGB. The output's saturation and perceived weight will match the seed, just at a different hue.

Once you have the semantic colour, feed it into tonal/2, theme/2, contrast/2, or contrast_scale/2 to produce a full scale for that semantic role.

Supported categories

Semantic aliases (UI vocabulary):

  • :success, :positive → green
  • :danger, :error, :destructive → red
  • :warning, :caution → orange
  • :info, :information → blue
  • :neutral → strips almost all chroma, preserving the seed's hue as a subtle tint

Hue categories (direct names):

  • :red, :orange, :yellow, :green, :teal, :blue, :purple, :pink

See semantic_categories/0 for the authoritative list at runtime.

Arguments

  • seed is anything accepted by Color.new/1.

  • category is a semantic alias or hue-category atom (see above).

Options

  • :chroma_factor multiplies the seed's chroma before the output is built. 1.0 (default) preserves it, 0.5 mutes the result, 0.0 produces a grey at the new hue.

  • :lightness overrides the output's lightness with a value in [0.0, 1.0] in Oklch. Defaults to the seed's lightness.

  • :gamut is the RGB working space to map into. Default :SRGB.

Returns

  • A colour struct in the chosen gamut (typically %Color.SRGB{}).

Examples

iex> {:ok, _} = Color.new("#3b82f6")
iex> danger = Color.Palette.semantic("#3b82f6", :danger)
iex> {:ok, oklch} = Color.convert(danger, Color.Oklch)
iex> oklch.h >= 15 and oklch.h <= 40
true

iex> success = Color.Palette.semantic("#3b82f6", :success)
iex> {:ok, oklch} = Color.convert(success, Color.Oklch)
iex> oklch.h >= 130 and oklch.h <= 160
true

iex> neutral = Color.Palette.semantic("#3b82f6", :neutral)
iex> {:ok, oklch} = Color.convert(neutral, Color.Oklch)
iex> oklch.c < 0.05
true

semantic_categories()

@spec semantic_categories() :: [atom()]

Returns the full list of category atoms accepted by semantic/3.

Returns

  • A list of atoms in alphabetical order.

Examples

iex> categories = Color.Palette.semantic_categories()
iex> :success in categories
true
iex> :red in categories
true

theme(seed, options \\ [])

@spec theme(
  Color.input(),
  keyword()
) :: Color.Palette.Theme.t()

Generates a complete Material Design 3 style theme from a seed colour. See Color.Palette.Theme for the full algorithm and option list.

Arguments

Options

See Color.Palette.Theme.new/2.

Returns

Examples

iex> theme = Color.Palette.theme("#3b82f6")
iex> match?(%Color.Palette.Theme{}, theme)
true

tonal(seed, options \\ [])

@spec tonal(
  Color.input(),
  keyword()
) :: Color.Palette.Tonal.t()

Generates a tonal scale — N shades of a single hue — from a seed colour. See Color.Palette.Tonal for the full algorithm.

Arguments

  • seed is anything accepted by Color.new/1 — a hex string, a CSS named colour, an %Color.SRGB{} struct, etc.

Options

  • :stops is the list of stop labels to generate, default [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] (Tailwind's convention).

  • :light_anchor is the Oklch lightness of the lightest stop, default 0.98.

  • :dark_anchor is the Oklch lightness of the darkest stop, default 0.15.

  • :hue_drift — when true, the hue drifts slightly toward yellow at the light end and toward blue at the dark end, matching how human vision perceives lightness. Default false.

  • :gamut is the working space to gamut-map each stop into, default :SRGB. Widening the gamut (for example :P3_D65 or :Rec2020) gives non-seed stops more chroma headroom and produces a smoother ramp for saturated seeds, at the cost of colours that may not display accurately on sRGB-only monitors.

  • :chroma_ceiling is a float in (0.0, 1.0] that caps each stop's chroma at ceiling × max_chroma(L, H, gamut). The default 1.0 lets stops hug the gamut boundary. Lowering it (for example 0.85) produces a more muted, evenly saturated-looking ramp.

  • :name is an optional string label stored on the struct.

Returns

Examples

iex> palette = Color.Palette.tonal("#3b82f6", name: "blue")
iex> palette.name
"blue"
iex> Map.keys(palette.stops) |> Enum.sort()
[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]