niji

A practical OKLCH color toolkit for Gleam.

OKLCH is a perceptual color space based on OKLab, offering more intuitive color manipulation than RGB or HSL. Changes to lightness, chroma, and hue produce visually consistent results.

Quick Start

import niji

// Create a color
let brand = niji.oklch(0.62, 0.19, 250.0, 1.0)

// Convert to hex for CSS
let hex = niji.to_hex(brand)
// -> "#3B82F6"

// Create a complementary color
let complement = niji.complementary(brand)

// Mix colors
let mixed = niji.mix(brand, complement, 0.5)

// Output CSS format
let css = niji.to_css(mixed)
// -> "oklch(62% 0.19 220deg)"

Color Spaces

This module provides two main color types:

Features

Gamut Mapping

Colors with high chroma may fall outside the sRGB displayable range. The to_rgb() function applies CSS Color Module Level 4 gamut mapping, which preserves lightness and hue while reducing chroma to fit within sRGB.

For simple channel clamping (faster but less accurate), use to_rgb_clamped().

Types

OKLCH color representation.

  • l: Lightness (0.0 = black, 1.0 = white)
  • c: Chroma/saturation (0.0 = neutral gray, can exceed 0.4 for wide-gamut colors)
  • h: Hue in degrees (0-360, where 0=red, 120=green, 240=blue)
  • alpha: Opacity (0.0 = transparent, 1.0 = opaque)
pub type Oklch {
  Oklch(l: Float, c: Float, h: Float, alpha: Float)
}

Constructors

  • Oklch(l: Float, c: Float, h: Float, alpha: Float)

RGB color representation.

  • r: Red channel (0.0-1.0)
  • g: Green channel (0.0-1.0)
  • b: Blue channel (0.0-1.0)
  • alpha: Opacity (0.0 = transparent, 1.0 = opaque)
pub type Rgb {
  Rgb(r: Float, g: Float, b: Float, alpha: Float)
}

Constructors

  • Rgb(r: Float, g: Float, b: Float, alpha: Float)

Values

pub fn analogous(color: Oklch, angle: Float) -> #(Oklch, Oklch)

Get analogous colors on both sides of the hue wheel.

For an angle of 30deg, this returns hues at -30deg and +30deg. Lightness, chroma, and alpha are preserved.

pub fn ansi(text: String, color: Oklch) -> String

Wrap text in ANSI escape codes to display it with the given foreground color.

pub fn ansi_bg(text: String, color: Oklch) -> String

Wrap text in ANSI escape codes to display it with the given background color.

pub fn ansi_fg_bg(text: String, fg: Oklch, bg: Oklch) -> String

Wrap text in ANSI escape codes to display it with both foreground and background colors.

pub fn complementary(color: Oklch) -> Oklch

Get the complementary color (hue + 180deg).

Lightness, chroma, and alpha are preserved.

pub fn contrast_ratio(color1: Oklch, color2: Oklch) -> Float

Calculate the contrast ratio between two colors using WCAG formula. Returns a value from 1.0 (no contrast) to 21.0 (maximum contrast).

pub fn darken(color: Oklch, amount: Float) -> Oklch

Darken a color by decreasing lightness. Clamps to 0.0 if the result goes below.

pub fn desaturate(color: Oklch, amount: Float) -> Oklch

Decrease chroma (saturation). Clamps to 0.0 if exceeded.

pub fn distance(color1: Oklch, color2: Oklch) -> Float

Calculate perceptual distance (deltaE OK) between two colors.

A value of 0.0 means the colors are identical in OKLab coordinates.

pub fn gamut_map(color: Oklch) -> Oklch

Map an OKLCH color into sRGB gamut and return it as OKLCH.

In-gamut colors are returned unchanged. Out-of-gamut colors are converted through the CSS-style gamut mapping path used by to_rgb/1.

pub fn gradient_fold(
  from: Oklch,
  to: Oklch,
  count: Int,
  acc: a,
  callback: fn(a, Oklch) -> a,
) -> a

Fold over a gradient of N colors between two OKLCH colors.

Generates count colors evenly spaced between from and to, then applies the callback function to each color with an accumulator. The callback receives (accumulator, color) and returns a new accumulator.

This is similar to int.fold but iterates over interpolated colors rather than integers. Hue interpolation handles the 0/360 boundary correctly by taking the shortest path.

Examples

Build a list of hex strings from a 5-step gradient:

let blue = niji.oklch(0.5, 0.2, 250.0, 1.0)
let red = niji.oklch(0.5, 0.2, 25.0, 1.0)

let hexes = niji.gradient_fold(blue, red, 5, [], fn(acc, color) {
  [niji.to_hex(color), ..acc]
}) |> list.reverse()
// Returns: ["#...", "#...", "#...", "#...", "#..."]

Compute the sum of lightness values:

let sum = niji.gradient_fold(from, to, 10, 0.0, fn(acc, color) {
  acc +. color.l
})

Edge Cases

  • If count <= 0, returns the initial accumulator unchanged
  • If count == 1, the callback is called once with the from color
  • The last color passed to the callback is always to (or very close to it)
pub fn grayscale(color: Oklch) -> Oklch

Convert a color to grayscale.

Removes all chroma while preserving the lightness (luminance). The resulting color has the same perceptual brightness but no hue.

Examples

let red = niji.oklch(0.5, 0.3, 0.0, 1.0)
let gray = niji.grayscale(red)
// gray has same lightness (0.5) but chroma = 0
pub fn has_hue(color: Oklch) -> Bool

Check if the color has a meaningful hue. Returns False when chroma is 0 (achromatic colors like grays). Per CSS spec, hue is “none” when chroma is 0.

pub fn in_gamut(color: Oklch) -> Bool

Check whether an OKLCH color is directly representable in sRGB gamut.

Returns True only when the converted RGB channels are all in [0.0, 1.0] before clamping.

pub fn invert(color: Oklch) -> Oklch

Invert a color.

Rotates hue by 180° (complementary color).

Examples

// Hue-only inversion
let inverted = niji.invert(color)
pub fn invert_full(color: Oklch) -> Oklch

Fully invert a color (hue + lightness).

Rotates hue by 180° and inverts lightness (1.0 - L).

Examples

let red = niji.oklch(0.5, 0.3, 0.0, 1.0)
let inverted = niji.invert_full(red)
// Results in cyan with lightness 0.5
pub fn lighten(color: Oklch, amount: Float) -> Oklch

Lighten a color by increasing lightness. Clamps to 1.0 if the result exceeds.

pub fn luminance(color: Oklch) -> Float

Get the relative luminance of a color. In OKLCH, lightness (L) directly corresponds to relative luminance.

pub fn mix(color1: Oklch, color2: Oklch, weight: Float) -> Oklch

Mix two colors together using linear interpolation. The weight parameter controls the mix: 0.0 returns color1, 1.0 returns color2. Weight is clamped to 0.0-1.0 range. Handles hue interpolation correctly across the 0/360 boundary.

pub fn oklch(l: Float, c: Float, h: Float, alpha: Float) -> Oklch

Create an OKLCH color with automatic clamping.

Values outside valid ranges are clamped:

  • L is clamped to 0.0-1.0
  • C is clamped to 0.0 or greater (no upper bound)
  • H wraps around (e.g., 400 becomes 40)
  • Alpha is clamped to 0.0-1.0
pub fn oklch_from_colour(colour: colour.Colour) -> Oklch

Convert a gleam_community_colour Colour to OKLCH. This conversion always succeeds.

pub fn oklch_to_colour(
  oklch_color: Oklch,
) -> Result(colour.Colour, Nil)

Convert OKLCH to a gleam_community_colour Colour.

pub fn rgb(r: Float, g: Float, b: Float, alpha: Float) -> Rgb

Create an RGB color with automatic clamping.

Values outside 0.0-1.0 are clamped.

pub fn rgb_from_ints(r: Int, g: Int, b: Int, alpha: Float) -> Rgb

Create an RGB color from integer values (0-255).

Values outside 0-255 are clamped.

pub fn rgb_to_hex(color: Rgb) -> String

Convert an RGB color to a hex string.

Output is #RRGGBB when alpha is 1.0, otherwise #RRGGBBAA. Channel bytes are rounded and uppercase.

pub fn rgb_to_oklch(color: Rgb) -> Oklch

Convert RGB color to OKLCH.

Uses the OKLAB color space as an intermediate step: sRGB -> Linear RGB -> OKLAB -> OKLCH

pub fn rotate_hue(color: Oklch, degrees: Float) -> Oklch

Rotate hue by the given degrees. Positive values rotate clockwise, negative counter-clockwise.

pub fn saturate(color: Oklch, amount: Float) -> Oklch

Increase chroma (saturation). Values below 0.0 are clamped to 0.0, no upper bound.

pub fn set_alpha(color: Oklch, alpha: Float) -> Oklch

Set the alpha (opacity) channel.

pub fn set_c(color: Oklch, c: Float) -> Oklch

Set the chroma (saturation) channel. Values below 0.0 are clamped to 0.0, no upper bound.

pub fn set_h(color: Oklch, h: Float) -> Oklch

Set the hue channel.

pub fn set_l(color: Oklch, l: Float) -> Oklch

Set the lightness channel.

pub fn split_complementary(
  color: Oklch,
  angle: Float,
) -> #(Oklch, Oklch)

Get split complementary colors around the complement.

For an angle of 30deg, this returns hues at +150deg and +210deg. Lightness, chroma, and alpha are preserved.

pub fn temperature(kelvin: Float) -> Oklch

Create a color from color temperature in Kelvin.

Approximates the color of black-body radiation at the given temperature using an algorithm by Tanner Helland. This is a mathematical approximation and not physically exact.

Typical Values

  • 2700K: Warm incandescent (orange-white)
  • 4000K: Cool white fluorescent
  • 6500K: Daylight (neutral white)
  • 15000K: Deep blue sky

Valid Range

  • 1000K - 40000K (values outside this are clamped)

Examples

let warm = niji.temperature(2700.0)    // Warm white
let daylight = niji.temperature(6500.0) // Neutral white
let cool = niji.temperature(15000.0)   // Cool blue
pub fn to_css(color: Oklch) -> String

Serialize OKLCH color to CSS string format.

Output format: “oklch(50% 0.2 180deg)” or “oklch(50% 0.2 180deg / 0.5)”

  • Lightness is shown as percentage (0% - 100%)
  • Chroma is shown as a number (no unit, can exceed 0.4)
  • Hue is shown as degrees or “none” when chroma is 0
  • Alpha is only shown when less than 1.0

Examples

to_css(oklch(0.5, 0.2, 180.0, 1.0))
// -> "oklch(50% 0.2 180deg)"

to_css(oklch(0.5, 0.0, 0.0, 1.0))
// -> "oklch(50% 0 none)"

to_css(oklch(0.5, 0.2, 180.0, 0.5))
// -> "oklch(50% 0.2 180deg / 0.5)"
pub fn to_hex(color: Oklch) -> String

Convert an OKLCH color to a hex string.

Output is #RRGGBB when alpha is 1.0, otherwise #RRGGBBAA. Channel bytes are rounded and uppercase.

pub fn to_rgb(color: Oklch) -> Rgb

Convert OKLCH color to RGB using CSS gamut mapping.

Uses the CSS Color Module Level 4 gamut mapping algorithm: “Binary Search Gamut Mapping with Local MINDE” https://www.w3.org/TR/css-color-4/#gamut-mapping

This algorithm preserves lightness and hue while reducing chroma to bring out-of-gamut colors into the sRGB gamut. The result is perceptually closer to the original color than simple clamping.

For colors already within the sRGB gamut, no modification is made. For colors outside the gamut, chroma is reduced until the color fits within sRGB or until the difference between the clipped and target color is below the Just Noticeable Difference (JND) threshold.

pub fn to_rgb_clamped(color: Oklch) -> Rgb

Convert OKLCH color to RGB using simple clamping.

This is the original behavior that simply clamps negative RGB values to 0.0 and values > 1.0 to 1.0. This is faster but produces less perceptually accurate results than the gamut mapping algorithm.

For CSS-compliant gamut mapping, use to_rgb/1 instead.

pub fn triadic(color: Oklch) -> #(Oklch, Oklch)

Get the two triadic colors (hue + 120deg and +240deg).

Lightness, chroma, and alpha are preserved.

pub fn wcag_aa(ratio: Float) -> Bool

Check if a contrast ratio meets WCAG AA standard for normal text (4.5:1).

pub fn wcag_aa_large_text(ratio: Float) -> Bool

Check if a contrast ratio meets WCAG AA standard for large text (3:1).

pub fn wcag_aaa(ratio: Float) -> Bool

Check if a contrast ratio meets WCAG AAA standard for normal text (7:1).

pub fn wcag_aaa_large_text(ratio: Float) -> Bool

Check if a contrast ratio meets WCAG AAA standard for large text (4.5:1).

Search Document