Color Picker
Edit modern color formats through authored controls. Custom element
<ui-color-picker value="oklch(62% 0.18 32)" format="oklch" aria-label="Brand color"> <div data-ui-part="preview"> <div data-ui-part="preview-bar"> <label data-ui-part="format-field"> <span data-ui-part="input-label">Color format</span> <select data-ui-part="format"> <optgroup label="Perceptual"><option value="oklch">oklch</option><option value="oklab">oklab</option><option value="lch">lch</option><option value="lab">lab</option></optgroup> <optgroup label="sRGB"><option value="hex">hex</option><option value="rgb">rgb</option><option value="hsl">hsl</option><option value="hwb">hwb</option></optgroup> <optgroup label="Wide gamut"><option value="p3">p3</option><option value="rec2020">rec2020</option></optgroup> </select> </label> <div data-ui-part="gamut-bar"> <button data-ui-part="clamp" value="srgb" type="button" aria-label="Clamp to sRGB" hidden>→ sRGB</button> <button data-ui-part="clamp" value="p3" type="button" aria-label="Clamp to P3" hidden>→ P3</button> </div> </div> <div data-ui-part="readout"> <span data-ui-part="gamut" aria-hidden="true"></span> <label> <span data-ui-part="input-label">Raw color value</span> <input data-ui-part="input" type="text" spellcheck="false" autocomplete="off" aria-describedby="brand-color-warning"> </label> <button data-ui-part="copy" type="button" aria-label="Copy color value"> <svg data-ui-part="copy-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <rect x="5.75" y="5.75" width="8.5" height="8.5" rx="2" /> <path d="M10.25 3.5A1.75 1.75 0 0 0 8.5 1.75h-4.75A1.75 1.75 0 0 0 2 3.5v6.75" /> </svg> <svg data-ui-part="copied-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="M2.75 8.75 6.25 12.25 13.25 4.25" /> </svg> </button> </div> </div> <div data-ui-part="channels"> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Lightness"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Lightness value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Chroma"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Chroma value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Hue"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Hue value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Alpha"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Alpha value"> </div> <p id="brand-color-warning" data-ui-part="warning" role="status" hidden></p> </div></ui-color-picker>Install
import '@timelessui/components/css/tokens.css'import '@timelessui/components/css/color-picker.css'import '@timelessui/components/define/ui-color-picker'Anatomy
Parts are authored in your own markup and identified by the selector below, so your CSS can target the same anatomy without loading Timeless styles. Required parts must be present for the component to work. Private data-ui-internal-* hooks are written by the runtime and must never be authored.
| Part | Required | Selector | Purpose |
|---|---|---|---|
preview | No | [data-ui-part~='preview'] | Region holding the format select, gamut controls, and readout. |
preview-bar | No | [data-ui-part~='preview-bar'] | Row above the readout. |
format-field | No | [data-ui-part~='format-field'] | Label wrapper around the format select. |
format | Yes | select | Native <select> listing the supported color formats. |
gamut-bar | No | [data-ui-part~='gamut-bar'] | Row holding the clamp buttons. |
clamp | No | [data-ui-part~='clamp'] | Button that clamps an out-of-gamut color into sRGB or P3. Shown only when needed. |
readout | No | [data-ui-part~='readout'] | Row holding the swatch, raw input, and copy button. |
gamut | No | [data-ui-part~='gamut'] | Current-color swatch. Decorative. |
input | Yes | input[type='text'] | Raw CSS color value, editable as text. |
input-label | No | [data-ui-part~='input-label'] | Screen-reader label for the raw input or format select. |
copy | No | [data-ui-part~='copy'] | Copies the raw value to the clipboard. |
copy-icon | No | [data-ui-part~='copy-icon'] | Idle state of the copy button. Decorative. |
copied-icon | No | [data-ui-part~='copied-icon'] | Confirmed state of the copy button. Decorative. |
channels | No | [data-ui-part~='channels'] | Container for the per-channel rows. |
channel | Yes | [data-ui-part~='channel'] | One channel row. The picker rewrites the rows when format changes, so author one row per channel of the widest format you support. |
channel-label | No | [data-ui-part~='channel-label'] | Channel name. Written by the picker; decorative. |
channel-range | Yes | input[type='range'] | Slider for one channel. |
channel-input | Yes | input[type='number'] | Numeric entry for one channel. |
warning | No | [data-ui-part~='warning'] | Out-of-gamut or unparsable-value message. |
trigger | No | [data-ui-part~='trigger'] | Optional button when the picker lives inside a popover. |
content | No | [data-ui-part~='content'] | Optional popover surface wrapping the picker. |
Attributes
Every value below is implemented by the stylesheets this component ships. Boolean attributes are presence-based: author the attribute with no value, or omit it.
| Attribute | Values | Default | Description |
|---|---|---|---|
format | oklch · oklab · lch · lab · hex · rgb · hsl · hwb · p3 · rec2020 | oklch | Color space the channel controls edit and the raw input round-trips through. The picker converts the current value when this changes. |
value | any string | — | Initial and form-reset color, in any CSS color syntax. Unlike the collection elements, the picker reflects one value property rather than a separate authored default. |
Element API
Attributes above are the authoring surface. These are the DOM properties and events the registered element adds once it upgrades.
<ui-color-picker>
| Property | Type | Notes |
|---|---|---|
format | 'oklch' | 'oklab' | 'lch' | 'lab' | 'hex' | 'rgb' | 'hsl' | 'hwb' | 'p3' | 'rec2020' | Reflects the format attribute, documented above. |
value | string | Reflects the value attribute, documented above. |
| Event | Detail | Description |
|---|---|---|
input | Event | Native input event dispatched while the color is being edited. |
change | Event | Native change event dispatched when the edit is committed. |
Styling
Root identity: ui-color-picker. Required stylesheets: tokens.css, color-picker.css.
This component adds no custom properties of its own. Restyle it through the design tokens or your own CSS.
Design tokens this component reads (24)
These are global Atmosphere tokens. Change them once at the theme level rather than per component.
--ui-bg-control · --ui-bg-surface · --ui-bg-surface-raised · --ui-bg-surface-subtle · --ui-checkerboard · --ui-danger · --ui-disabled-opacity · --ui-fg · --ui-fg-muted · --ui-focus · --ui-font-mono · --ui-line · --ui-line-strong · --ui-radius-lg · --ui-radius-md · --ui-radius-pill · --ui-radius-sm · --ui-shadow-control · --ui-shadow-floating · --ui-space-2 · --ui-space-3 · --ui-space-4 · --ui-warning · --ui-warning-soft
Accessibility
Keep the native elements, roles, and relationships shown in the example. Timeless adds only the state and keyboard coordination the platform does not already provide, and it never supplies your accessible names — those depend on your content.
Before JavaScript runs
The markup above is complete and usable on its own: native controls submit, labels associate, and authored ARIA is already correct. Registration adds state synchronisation, focus management, and keyboard coordination on top of it.
Popover Color Picker
Compose Color Picker and Popover.
<ui-popover> <button class="ui-color-swatch" type="button" data-ui-part="trigger" aria-label="Edit Brand color" style="--ui-color-swatch: oklch(62% 0.18 32)"> <span data-ui-part="chip" aria-hidden="true"></span> <span data-ui-part="label">Brand color</span> <span data-ui-part="value">oklch(62% 0.18 32)</span> </button> <ui-color-picker value="oklch(62% 0.18 32)" format="oklch" aria-label="Brand color" data-ui-part="content" popover role="dialog"> <div data-ui-part="preview"> <div data-ui-part="preview-bar"> <label data-ui-part="format-field"> <span data-ui-part="input-label">Color format</span> <select data-ui-part="format"> <optgroup label="Perceptual"><option value="oklch">oklch</option><option value="oklab">oklab</option><option value="lch">lch</option><option value="lab">lab</option></optgroup> <optgroup label="sRGB"><option value="hex">hex</option><option value="rgb">rgb</option><option value="hsl">hsl</option><option value="hwb">hwb</option></optgroup> <optgroup label="Wide gamut"><option value="p3">p3</option><option value="rec2020">rec2020</option></optgroup> </select> </label> <div data-ui-part="gamut-bar"> <button data-ui-part="clamp" value="srgb" type="button" aria-label="Clamp to sRGB" hidden>→ sRGB</button> <button data-ui-part="clamp" value="p3" type="button" aria-label="Clamp to P3" hidden>→ P3</button> </div> </div> <div data-ui-part="readout"> <span data-ui-part="gamut" aria-hidden="true"></span> <label> <span data-ui-part="input-label">Raw color value</span> <input data-ui-part="input" type="text" spellcheck="false" autocomplete="off" aria-describedby="popover-brand-color-warning"> </label> <button data-ui-part="copy" type="button" aria-label="Copy color value"> <svg data-ui-part="copy-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <rect x="5.75" y="5.75" width="8.5" height="8.5" rx="2" /> <path d="M10.25 3.5A1.75 1.75 0 0 0 8.5 1.75h-4.75A1.75 1.75 0 0 0 2 3.5v6.75" /> </svg> <svg data-ui-part="copied-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="M2.75 8.75 6.25 12.25 13.25 4.25" /> </svg> </button> </div> </div> <div data-ui-part="channels"> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Lightness"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Lightness value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Chroma"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Chroma value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Hue"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Hue value"> </div> <div data-ui-part="channel"> <span data-ui-part="channel-label" aria-hidden="true"></span> <input data-ui-part="channel-range" type="range" aria-label="Alpha"> <input data-ui-part="channel-input" type="number" inputmode="decimal" aria-label="Alpha value"> </div> <p id="popover-brand-color-warning" data-ui-part="warning" role="status" hidden></p> </div></ui-color-picker></ui-popover>for (const popover of document.querySelectorAll('ui-popover')) { const picker = popover.querySelector('ui-color-picker') const trigger = popover.querySelector("[data-ui-part~='trigger']") if (!picker || !trigger) continue const value = trigger.querySelector("[data-ui-part~='value']") const sync = () => { trigger.style.setProperty('--ui-color-swatch', picker.value) if (value) value.textContent = picker.value } picker.addEventListener('input', sync) picker.addEventListener('change', sync)}More examples
Open Color Picker in StoryLite for variants, controls, and manual testing.