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:
Oklch- Lightness, Chroma, Hue, and Alpha. The primary type for manipulationRgb- Red, Green, Blue, and Alpha. Used for output and interop
Features
- Type constructors:
oklch(),rgb(),rgb_from_ints() - Conversion:
to_rgb(),to_hex(),to_css(),rgb_to_oklch() - Manipulation:
lighten(),darken(),saturate(),desaturate(),rotate_hue() - Blending:
mix()for linear interpolation between colors - Palettes:
complementary(),triadic(),analogous(),split_complementary() - Gamut mapping: CSS-compliant gamut mapping for out-of-gamut colors
- Accessibility:
contrast_ratio(),wcag_aa(),wcag_aaa() - Terminal output:
ansi(),ansi_bg(),ansi_fg_bg() - Interop:
oklch_from_colour(),oklch_to_colour()for gleam_community/colour
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)
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 thefromcolor - 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_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 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).