A conventional palette generator often encourages us to think in swatches: choose a blue, choose an orange, then ask for some colors “between” them. Poline thinks more like a journey.

David Aerne’s micro-library takes anchor colors, turns them into coordinates, draws a path between those coordinates and samples the path to recover new colors.12

That distinction sounds poetic until two opposite anchors send the path through black.

This is where Poline becomes genuinely interesting: it is not trying to be a scientifically uniform color scale. What it exposes is a small geometry whose path can be deliberately reshaped.

Not OKLCH

One easy 2026 misunderstanding is worth removing first.

Poline does not interpolate in OKLCH. The current npm release, 0.13.1, is an MIT-licensed runtime-dependency-free library whose core color model remains HSL.23

The code maps an [hue, saturation, lightness] anchor into an internal three-coordinate space. Hue becomes an angle around a circle, lightness controls distance from its centre, and saturation becomes the z axis.2

Intermediate x, y and z values are calculated there before being converted back to HSL.2

Diagram explaining Poline's HSL to internal XYZ coordinate transform and conversion back to HSL
Poline's XYZ is an internal geometric space and should not be confused with colorimetric CIE XYZ.IRZ analysis and illustration from Poline 0.13.1

The API name xyz can therefore be deceptive if read with color-science expectations: these are not CIE XYZ values, merely Cartesian coordinates for Poline’s own polar construction.2

That construction is exactly why it can produce paths that direct HSL component interpolation would not naturally produce.

A line

When the same position function drives all three axes, Poline keeps the generated points on a straight line between two anchors in its internal space. The function changes their distribution along that segment.2

The default is sinusoidal, but users can switch to linear, exponential, quadratic, cubic, quartic, smoothstep and several other progressions.2

That already changes a palette. Ten samples equally spaced in time do not have to be equally spaced along a segment when easing accelerates or slows their movement.

The stranger behavior begins when x, y and z receive different functions.

A curve

The documentation gives exactly such an example: sinusoidal movement on x, quadratic movement on y, and linear movement on z.2

At a given moment the three coordinates have then travelled different fractions of their respective distances, pulling the point away from the straight segment and into a curve.

Once those axes move differently, the question shifts from merely asking which colors sit between A and B to asking which route the palette should take from A to B, because intermediate coordinates now depend on the chosen motion of each axis.

To make that less abstract, we used an anchor pair directly from Poline’s own README.

Blue orange

The documentation shows a gradient between:

HSL(210, 80%, 60%) → HSL(30, 80%, 60%)

Blue and orange sit exactly 180 degrees apart in hue.2

We generated ten swatches in three ways: naive HSL interpolation keeping saturation and lightness constant, Poline with linear positioning, and Poline with the documented mixed axis functions above.

Three paths between HSL 210 blue and HSL 30 orange: naive HSL, linear Poline and curved Poline
The same anchors tell three different stories. Naive HSL travels through cyan, green and yellow; linear Poline dives toward black; the curved route passes through violet, magenta and red.IRZ benchmark using Poline 0.13.1

Naive HSL holds L=60% and travels through cyan, green and yellow. Linear Poline behaves very differently: because the anchors oppose each other on its circle, the internal straight line approaches the centre and HSL lightness falls to 6.67% before rising again.

With Poline’s default sinusoidal positioning, one point in the same test reaches a numerically near-zero lightness, effectively producing black in the middle of the journey.

The result follows directly from the coordinate model because the centre of Poline’s circle corresponds to L=0.2

Nothing has gone wrong in rendering here; the plunge follows from a creative geometry that was never designed to preserve a particular perceptual property, which may be useful or inconvenient depending on the project.

Real distances

We then converted the resulting sRGB swatches into Oklab and measured Euclidean distance between adjacent colors. Oklab is designed so comparable distances correspond more closely to comparable perceived differences than they do in HSL.57

For this particular example, the ratio between largest and smallest adjacent step was:

  • 6.55× for our naive HSL interpolation;
  • 2.46× for linear Poline;
  • 2.78× for the curved Poline path.

The result is amusing: Poline happened to produce less uneven steps than naive HSL here despite not operating in a perceptually uniform space.

Those numbers should not be read as a general ranking, since different anchors, easings or sample counts change the measurements and Poline does not optimize any perceptual ΔE in its algorithm.2

The defensible conclusion is narrower: different geometry produces different distribution, and properties that matter to a final application need to be measured rather than inferred from the name of a color model.

Pseudo OKLCH

Release 0.13.1 contains another detail that matters when these palettes are exported to the web: colorsCSSoklch and oklchCSS are real APIs.23

The code comments are equally explicit that the output is an approximation linearly rescaled from HSL, not a true colorimetric conversion.2

Poline maps:

  • HSL lightnessOKLCH L;
  • HSL saturation × 0.4OKLCH C;
  • HSL hueOKLCH h.

Those channels do not mean the same thing: MDN specifically notes that OKLCH lightness represents perceived lightness, whereas HSL lightness is defined differently and does not track perceived brightness across hues.45

Our blue anchor makes the mismatch concrete because Poline exports:

oklch(60% 0.320 210)

A true colorimetric conversion of the original HSL sRGB color gives approximately, in our calculation:

oklch(66.96% 0.145 250.90)

The orange tells the same story: Poline emits 60% 0.320 30, while the converted original is around 75.06% 0.137 62.81.

The generated CSS syntax is valid, but it describes different colors. Poline’s README itself recommends a conversion library such as Culori when another color model is needed.2

Contrast apart

Accessibility forms a second boundary.

A palette can be harmonious, strange and delightful while being completely unsuitable behind text. WCAG 2.2 Level AA requires a contrast ratio of at least 4.5:1 for normal text, with a 3:1 threshold for large text.6

We calculated black and white text contrast against all ten swatches from our curved path.

Ten curved Poline colors with WCAG contrast ratios for black and white text
No single foreground color works across the whole path. White passes on 7/10 swatches and black on 3/10; selecting between black and white per swatch lets all ten exceed 4.5:1 in this example.IRZ calculation and illustration using the WCAG 2.2 formula

A fixed white foreground passes on seven of the ten swatches, while a fixed black foreground passes on only three, so neither can simply be assigned to the complete palette.

Every swatch in this particular set does have at least one choice between black and white that clears 4.5:1. The result neatly separates responsibilities: Poline generates a path; a design system still has to choose roles, foregrounds and valid color pairs.

Not a system

That may be Poline’s best role: a generator of material rather than a system validator.

For generative graphics, animation, poster series or themes that need continuous motion, getColorAt(t) is especially attractive. A value from 0 to 1 can sample any point along the complete color journey, including multi-segment paths with easing.2

For interface tokens, the job continues afterwards. Contrast must be checked, perceptual distances may need controlling, text-bearing colors must be identified, interaction states tested, and visually distinct swatches cannot simply be assumed to remain distinct for every user.

Moving the next stage into OKLCH gives lightness and geometric distance a more perceptually meaningful basis than HSL.45 Merely writing oklch(...) is not enough, however; the colors need to be genuinely converted or constructed in that space.

Useful detour

Poline is a small MIT-licensed library with no runtime dependency, and that modest scope is part of its appeal.23

Its README even describes the method with deliberate self-mockery as a kind of polar-coordinate witchcraft.2

The actual mechanism is pleasantly compact: turn HSL into points, connect anchors, apply easing and convert the sampled points back to colors.

Its strongest contribution is therefore less a particular palette than the reminder that between two colors, choosing the path is already a design decision, and that an intermediate swatch is never quite as neutral as a row of boxes makes it appear.