Colour
The picker (click a swatch):
- Planes: HSV, or OKLCH. In OKLCH, lightness runs down and chroma across, at a hue. The hue strip is an even-lightness rainbow.
- The picker remembers which plane you used.
- Across the OKLCH plane is chroma out of the most sRGB has at that lightness, so every point is a colour sRGB holds. The right edge is the most colourful one there; the C field gives the chroma itself.
- Changing the lightness or hue keeps how colourful the colour is out of the most there is. A sweep through a hue with less room (cyan has half blue's) comes back to the colour it left.
- With
color: { gamut: 'p3' }and a P3 display, the plane runs to Display P3's most (for float colours and three.js), with sRGB's edge drawn as a line, named "sRGB".
- Fields: the colour as text (a hex, or
color(display-p3 …)past sRGB; labelled "hex" on a value that holds only sRGB), then RGB bytes or OKLCH L/C/H, all scrubbable. - Alpha, on a colour that has it: a strip under the hue, over a checkerboard, and an alpha number after the channels, 0 to 1 as CSS writes it (
/ 0.5). The text carries it too (#3080ff80).- A colour has alpha where its value has somewhere to keep it:
#rgbaand#rrggbbaa,rgba(),hsla(), anything with/ a,{ r, g, b, a },[r, g, b, a], and{ x, y, z, w }(a shader'svec4, named or hinted as a colour). - A value without it never gains it:
#rrggbb,{ r, g, b },[r, g, b], a hex number, three.jsColor. A CSS string can be asked to:color: { alpha: true }lets#3080ffgrow to#3080ff80when you make it transparent. Asked of a value with nowhere to keep it, the hint is ignored and its explain text says so.color: { alpha: false }hides it on any value. - Alpha pasted, set by an agent or loaded onto a colour without it is left out, and a paste's toast says so: "tint holds no alpha (hex string): pasted without its alpha (0.5)".
- A translucent colour's swatch is split corner to corner: the colour itself in one half, the colour over the checkerboard in the other. So a faint colour still shows which colour it is. Its handles in the ramp editor are split the same way.
- A colour has alpha where its value has somewhere to keep it:
- The eyedropper (where the browser has one) picks from anywhere on screen. It picks the colour and keeps your alpha: a pixel on screen is everything under it, opaque.
- Recent colours, the "recent" row at the bottom: each picker you close on a new colour adds it, latest first, as many as fit on the line. They're kept per project, whole (HDR, P3 and alpha too), and shared by every colour. On a colour without alpha they show and give their colours alone. + adds the colour you're editing. Click one to use it; drag it off the row (as a ramp's stop), or press Delete, to remove it.
- HDR intensity in EV, on every value that can hold HDR: floats, three.js and
color()strings, HDR yet or not. Scrub it up to go past white;color: { hdr: false }hides it (on a base colour, say). The hex field is then the colour under the intensity (labelled "base"): a hex typed there keeps the intensity.
Colour spaces, HDR and the clipboard.
- The row shows the colour itself, in the simplest standard CSS that holds it:
- sRGB:
#ff7448(a float colour too, to 8 bits; Copy as code stays exact). A string value shows its own text:rgb(255 116 72),oklch(0.7 0.15 40). - Display P3:
color(display-p3 0 1 0), with a P3 badge on the swatch. Measured in P3, so a P3 green isn't HDR, though its green is over 1 in sRGB. On a P3 screen the swatch shows it as it is; on an sRGB screen, as the nearest colour there with its lightness and hue (as every view of a colour does: the stop bar, the wheel, the Check tab). - HDR:
color(srgb-linear 2.4 1.1 0.3), with an HDR badge, in the value's own space (linear for three.js andspace: 'linear-srgb', sRGB-encoded for other floats), so the numbers are the stored ones. The badge's tooltip says the rest: "HDR ×2.4 (+1.26 EV) over #ffb463". - Three decimals to read; in full while you edit it, and in the tooltip.
- sRGB:
- On an HDR display, an HDR colour's swatch shows brighter than white: a small WebGPU canvas in extended range (WebGPU's HDR canvas, Chrome 129 on), on devui's own device. So do the recent colours, and the picker's plane at the colour's intensity.
createGUI({ hdr: 'off' })turns it off; elsewhere the swatch is as on an SDR display. - Copy: Cmd/Ctrl+C on a swatch, or Copy value, copies the row's text, as plain CSS. Copy as code gives your value's own literal, losing nothing:
0xff7448,[2.4, 1.1, 0.3],new THREE.Color(2.4, 1.1, 0.3). - Paste: Cmd/Ctrl+V on a swatch, Paste value, or type into the row's text. It takes hex (with or without
#,0xRRGGBB),rgb(),hsl(),oklch(),oklab(),color(srgb | srgb-linear | display-p3 …),[r, g, b]in the value's own numbers, and saved values. - What a value holds: a hex string,
rgb(), a number or bytes hold 8-bit sRGB; anoklch()string any gamut but no HDR; floats, three.js andcolor()strings anything.- The picker offers only what the value holds: no intensity on a hex string, and its plane stops at sRGB, whatever the hints say (its explain text, in the row's menu, names those ignored).
- Anything else that arrives is fitted, keeping its hue: HDR over its largest channel (an HDR orange stays that orange), then less chroma in OKLCH. A toast says what it took ("tint holds sRGB only (hex string): pasted without its ×2.4 intensity"), and one undo puts it back.
- For P3 in a string, write its first value as
color(display-p3 …)oroklch(…): its format is kept, and its picker reaches P3.
Ramps and palettes. For trying colour schemes quickly:
import { gui, ramp } from '@haxiomic/devui';
const look = gui.folder('Look').params({
heat: ramp(['#0d0887', '#cc4778', '#f0f921']), // a colour ramp, sampled by your code
palette: ['#203040', '#408080', '#e0c060', '#f0a050'], // a list of colours: edited in place
});
look.heat.hex(0.3); // '#rrggbb' at t: the colour alone
look.heat.sample(t); // linear RGBA, alpha straight
look.heat.colors(5); // 5 colours, end to end, as '#rrggbb'
look.heat.lut(256); // Float32Array, RGBA linear, alpha straight
look.heat.gradient(); // a CSS linear-gradient(…), with alpha
Your own gradients. A gradient your code already has is edited in place, and your code draws it as it did, with nothing of devui's:
const look = gui.folder('Look').params({
sky: [{ offset: 0, color: '#1b2a4a' }, { offset: 1, color: '#ffd9a8' }], // stops
banner: 'linear-gradient(to right, #2bd9a5, #c43f70)', // CSS
});
for (const s of look.sky) grad.addColorStop(s.offset, s.color); // canvas, as before
el.style.background = look.banner;
- A list of stops: objects with a place (
offset,t,position,pos,atorstop: 0 to 1, or 0 to 100) and a colour (colororcolour: a CSS string, not a named one, or a0xRRGGBBnumber), or[t, '#hex']pairs. A stop you add is shaped as the last, its other fields too, and each colour keeps its format. - It blends as your code blends it, so the editor shows that blend and can't change it: sRGB unless you say otherwise, as canvas, SVG and CSS draw. Say yours with
{ value: stops, ramp: { mode: 'oklab' } }. - A CSS gradient (
linear-,radial-orconic-gradient(…)) keeps its direction or shape and each colour's format. Its blend is CSS's: choose another in the editor and it writesin oklch, say. Repeating gradients, colour hints, lengths (20px), named colours and other blends stay text. - Production builds apply your saved edits to the same data. Copy as code gives the data itself, to paste into your source.
Common tasks:
- A palette for a sketch, fast: click the palette's row. The library opens on categorical sets: hover one to see it in your app, click to keep it, or step through them with the arrow keys. Shuffle picks one at random.
- A palette from a colour you have: type or paste yours into a colour's field, and double-click that colour to pin it. On the Wheel tab, choose guides (a triad, say): lines appear from your colour, and nothing moves. Drag the others near the lines and they snap on, or hover Deal round the lines to see them all placed, and click to keep it. Your colour stays as it is.
- Colours from another tool: paste them into the library's search (hexes, CSS colours, a Coolors link) and click the palette that appears at the top.
- A ramp for a shader (a heat map, a sky): add stops on the bar, type exact places in the at field, and choose how it blends (smooth, for no corners at the stops). The Wheel tab shows its path through the hues and its lightness along it. On the Check tab, Even out gives it an even pace. Sample it in your code with
lut()orrampTexture, and bake it into your source with Copy as code. - Colours for data: filter the library by sequential or diverging, or search "cvd" for maps made for colour-blind eyes. On the Check tab, lightness that only rises reads in order for everyone, and the colour-vision rows show the ramp as others see it.
- A fade to transparent (a particle's colour over its life, a glow): select a stop and drag its alpha down, or write it:
ramp(['#fff3b0', '#ffb030', '#e0302000']). Then try palettes: one with no alpha of its own takes your ramp's, so the fade stays.- It fades without going dark. Ramps blend premultiplied, as CSS gradients do: each stop's colour counts as much as it's seen. A transparent stop's colour, hue too, isn't seen at all, so
ramp(['#f00', '#0000'])is red fading out, not red going through dark red. A ramp whose stops are all opaque blends as before. - In a shader:
sample(),lut()andrampTexturegive straight alpha, as the stops are: multiply by it for premultiplied blending. At a transparent end the colour is the one it fades from, so a lookup texture filtered there doesn't darken.hex()andcolors()give the colour alone, for code that reads six-digit hexes. - Past sRGB:
hex(),colors(),gradient()andrampTextureare 8-bit sRGB, as a screen shows the colour (and as the panel draws it): a stop outside sRGB (a Display P3 green) is brought in with less chroma, its lightness and hue kept, and an HDR stop gives its colour without the intensity (an orange at ×2 gives that orange). No channel is clipped on its own, which would turn the hue.sample()andlut()give the stops as they are.
- It fades without going dark. Ramps blend premultiplied, as CSS gradients do: each stop's colour counts as much as it's seen. A transparent stop's colour, hue too, isn't seen at all, so
- Editing: click the gradient to edit it.
- Stops: click the bar to add a stop, drag one to move it, drag it off to delete it. Click a stop to edit its colour and its alpha (every ramp has alpha; a list of colours when its strings do;
color: { alpha: false }hides it). A stop added on the bar takes the colour and alpha the ramp has there, so it changes nothing. Its place along the ramp (0 to 1) is the at field under the bar: scrub it, or click to type an exact one, or move a focused stop with the arrows (Shift faster, Alt slower). Double-click a stop (or press P) to pin it: guides and starters build round the pinned colour. - Blend in: how the colours between stops are mixed (the stops are the same in every one). OKLab runs straight across the colour plane (even steps; between far-apart hues it passes nearer grey), OKLCH round the hue wheel the short way, OKLCH long the long way (more hues), linear as light, sRGB averages the encoded numbers as a CSS gradient does (far-apart hues sag into dull grey), and step makes bands. Smooth makes a curve through the stops, with no corners at them, that never goes past them:
ramp(stops, 'oklab', { smooth: true }). - Lists: a list of colours keeps its length. Drag a band sideways to reorder it.
- The handles: a colour in a white rim, the same on the bar, the wheel and the graph. The selected one is lifted (larger, with a shadow); the pinned one has a dotted ring. A translucent one is split, as its swatch is.
- Stops: click the bar to add a stop, drag one to move it, drag it off to delete it. Click a stop to edit its colour and its alpha (every ramp has alpha; a list of colours when its strings do;
- Beside the editor, three tabs. Reverse, Shuffle, Generate and Save sit above them all. Shuffle picks a palette at random (from those the library shows, when it's filtered). Generate makes a new one (harmonious OKLCH hues, or a cosine palette). Reverse flips it. Save keeps yours in the library, per project, alpha too. Each of these, the library and the starters keep your ramp's alpha when what they give has none of its own.
- Library: about 880 palettes, by kind, then source. Gradients, there for their looks, are by look instead: by hue family round the wheel (reds to pinks), then those whose colours span more than 100° of hue (blue to orange), then greys. Each family runs dark to light, with those that look alike side by side.
- Kinds: sequential, diverging, cyclic, categorical, gradients and UI scales. A list of colours opens on the categorical sets, a ramp on the sequential maps.
- Scientific maps: matplotlib's viridis family, seaborn, ColorBrewer, CARTO, Fabio Crameri's scientific colour maps, cmocean and Peter Kovesi's CET maps. Each is the fewest stops that redraw it, blended smoothly in OKLab: five or so to edit (more for a rainbow), not the hundreds of samples it's published as.
- Categorical sets: Paul Tol's sets, Okabe–Ito, Tableau and Observable, Glasbey, and editor themes' accents.
- Gradients: uiGradients and webgradients, devui's OKLCH palettes and cosine palettes (each tile's tooltip names its source; search finds them by it).
- UI scales: Tailwind, Radix and Open Color.
- Credits and licences: in
THIRD_PARTY_NOTICES.md. The data loads the first time the library opens. - Find:
- Search by name, source, hue or tone: "blue dark", "crameri", "pastel", or "cvd" for those made for colour-blind vision. ↓ goes to the results.
- Paste colours into the search to use colours you already have: hexes (with or without
#), CSS colours, a CSS gradient or a Coolors link, with their alpha. Two or more become a palette at the top of the results. A list takes all of them, however many; a ramp takes them as its stops, spread evenly, in its own interpolation. - Filter by a kind or a hue dot.
- Star favourites with F, or the ☆ on a tile, then show only them with ★. Favourites are kept across projects.
- Peeking: browsing everything, each source shows its first two rows (+N shows the rest). A filter shows all.
- Hover a palette to see it in your app; move away and yours comes back.
- Click applies it (one undo). From a focused palette, the arrow keys step through them, applying each one.
- On a list of colours: a categorical palette gives its colours in order. Anything else is sampled along it.
- Wheel: your colours on a hue wheel and a lightness graph. Every colour is a handle you can grab.
- The wheel is OKLab's colour plane: hue round, chroma out from the grey middle. A ramp's path runs across it, so you see what its blend does: OKLab is a straight line between stops, OKLCH an arc. Drag a colour round for its hue and in or out for its chroma; drag the empty wheel to turn every hue. A colour dragged further out than the screen can show keeps the chroma you gave it there, and shows as the nearest colour the screen has.
- The graph is lightness, black at the bottom. Drag a colour up or down. A ramp's stops move along it too; drag the empty graph to lighten or darken them all.
- Alpha: the wheel and the graph place a colour by its colour alone. The path is the colours the ramp gives, so it bends less toward a translucent stop, and a fully transparent stop's handle sits off it: its colour is never seen.
- Guides (mono, complement, split, triad, square, analogous) draw lines from the pinned colour's hue. Choosing them moves nothing. Drag a colour near a line and it snaps on (the line turns solid); drag it away and it comes off. Colours on a line follow when you turn the pinned colour (on the wheel, or in the picker) or change the spread.
- To move them all onto the lines: Snap to nearest lines moves each colour the least; Deal round the lines gives the colours after the pinned one the other lines in turn, so every line is used. Each shows its result: hover to see it in your app, click to keep it (one undo). Only hues change; lightness and chroma stay.
- Starters build round the pinned colour, which they keep as it is, so a second click changes nothing. For a ramp: dark to light, to white, diverging, a hue sweep. For a list: even lightness (for categories), dark to light, distinct hues, shades.
- Check: how the ramp reads, and fixes. It measures the colours alone: what's behind a translucent one isn't known. Its fixes keep each stop's alpha.
- The chart: lightness, chroma and pace (colour difference, ΔE, along it). The summary says whether lightness only rises or falls (then it reads in order for everyone), and how even the pace is. On a list, it gives the closest two colours.
- Fixes: Even out places the stops by colour difference, so the ramp changes at an even pace. Straight lightness puts each stop's lightness on a line from the first's to the last's.
- Colour vision: the ramp as seen with protanopia, deuteranopia, tritanopia (Machado et al.'s simulation) and in greyscale. A list shows each view's closest two colours (ΔE × 100). Under 3 is hard to tell apart.
- Production builds:
ramp()works in the noop build too, so your code can sample it. To draw a saved ramp with none of the panel's code, import from@haxiomic/devui/ramp:Ramp,rampand the functions on a ramp's value, such asrampSamplerandsampleRamp.rampSampleralone is about 2 KB gzip. - three.js:
rampTexture(look.heat)(from@haxiomic/devui/three) is a 1D lookup texture that updates as you edit. Its first and last texels' centres hold the ramp's ends, so read it through those centres.texture(ramp, vec2(t, 0.5))alone reads up to half a texel off, which shows at a hard edge:
glsl const float size = 256.0; // rampTexture's size vec4 colour = texture(ramp, vec2((t * (size - 1.0) + 0.5) / size, 0.5));