registry:ui · Data Entry
color-picker — native field, pro editor
Color picking on NATIVE controls (2026-09-01 rebase — jx-pure is the foundation; the registry surface adds slots and semantics, never re-drawn inputs): the value field is a real input[type=text] (label[for], name= rides its own FormData lane, native focus/selection/disabled) and the swatch is a real input[type=color] that opens the engine picker; the chevron opens the pro popover editor — SV pad + hue bar (custom 2D picker surfaces, the legitimacy class of date-picker's calendar), hex/hsl/oklch switching with OKLCH as the conversion hub, invalid-draft revert, Eye Dropper API. One truth: every surface flows through the bindable value string.
$npx jixoai-ui add color-picker
https://ui.jixoai.com/r/color-picker.json
overview
Overview
One value string, four native-or-custom input paths, zero second truths: the lane rides the platform's own controls, the editor is the custom surface, and oklch is the conversion hub.
The lane is a REAL input[type=text] — label[for] binds it, name= submits
through its own FormData lane, focus, selection and disabled are the platform's — and
the swatch is a REAL input[type=color] styled to the generated COLOR LAW
face, so clicking it opens the engine's picker: every input mode gets a picker. The
chevron opens the pro editor in a terminal-bezel popover — a saturation/value pad and a
full-spectrum hue rail, 2D picker surfaces no native element provides, the same
legitimacy class as date-picker's calendar grid.
OKLCH is the conversion hub — the token system's space — so every notation round-trips through one canonical model with zero dependencies, and every surface (field typing, swatch pick, editor drag, bind write) flows through the ONE value string. Invalid drafts revert; a format switch re-emits the same color in the new notation.
The family's anatomy follows the density ruler's ambient channels — the well is the ruler's floor equation — while the eight-axis carriers it stamps have zero family readers: this is a color INSTRUMENT, and its chrome is deliberately hue-neutral. The per-axis receipts are below.
usage
Usage
Bind a string value and choose the notation the picker emits.
<!-- value notation follows format; oklch is the conversion hub -->
<ColorPicker label="brand" bind:value={brandColor} />
<ColorPicker label="accent" bind:value={accentColor} format="oklch" name="accent" />
<!-- the lane: a REAL input[type=text] field (label[for], name=, native
focus/selection) + a REAL input[type=color] swatch that opens the
ENGINE picker on click — every input mode gets a picker -->
<ColorPicker label="swatch only" bind:value={c} showValue={false} />
<!-- paste any notation into the field — invalid drafts revert; the
chevron opens the pro editor (Eye Dropper when present) -->
<ColorPicker label="theme hue" bind:value={c} format="hsl" />color-picker
the value field is a REAL input[type=text] (label[for], name=, native focus and selection) and the swatch is a REAL input[type=color] that opens the ENGINE picker on click; the chevron opens the pro editor — SV pad + hue bar, hex/hsl/oklch round-trips, direct value entry that reverts invalid drafts, and Eye Dropper when the platform has it.
<ColorPicker
label="brand"
bind:value
/>lane shapes
Lane shapes
The lane can show the native swatch (input[type=color]), the native value field (input[type=text]), or both; the value model supports three notations. This canvas composes its drawer from the stage markup itself — the code shown is the code running.
color-picker · types
<script lang="ts">
import ColorPicker from '@ui/color-picker/index';
</script>
<div class={cx(rt.cpGridSm)}>
<div class={cx(rt.panel)}><ColorPicker label="hex" value="#007924" format="hex" /></div>
<div class={cx(rt.panel)}><ColorPicker label="hsl" value="hsl(145 100% 24%)" format="hsl" /></div>
<div class={cx(rt.panel)}><ColorPicker label="oklch" value="oklch(0.6489 0.237 145)" format="oklch" showValue={false} /></div>
</div>catalogue
Bindings and error wiring
Bound values echo live; the error law dashes the lane and wires the native validation relations — the input.svelte merge, one law across every form family.
color-picker · catalogue
#007924<script lang="ts">
import ColorPicker from '@ui/color-picker/index';
let brandColor = $state('#007924');
let accentColor = $state('oklch(0.6489 0.237 145)');
let swatchOnly = $state('#b7d7a8');
</script>
<ColorPicker label="brand (hex)" bind:value={brandColor} />
<ColorPicker label="accent (oklch)" bind:value={accentColor} format="oklch" />
<ColorPicker label="swatch only" bind:value={swatchOnly} showValue={false} />Type in the field: parsed text commits canonically in the active notation and invalid drafts revert on change — native focus and selection throughout. Click the swatch: the engine's own color picker opens (a real input[type=color]; the pick re-emits through the same one-truth value, so a hex pick never rewrites an oklch picker's notation). The chevron opens the editor — drag the SV pad (saturation right, value up, pinned to ltr — the lane, not the map, is what rtl mirrors) or the hue bar, both through Pointer Events with capture. The panel anchors under the lane with CSS Anchor Positioning (flip-block fallback; engines without it get the authored viewport-center), and focus restitutes to the field on every close path.
error wiring
color-picker · error wiring
theme hue is required
<script lang="ts">
import ColorPicker from '@ui/color-picker/index';
let errorColor = $state('#8a5a2f');
</script>
<!-- error dashes the lane border and wires aria-invalid + describedby -->
<ColorPicker label="theme hue" error="theme hue is required" bind:value={errorColor} />Same law as every family member: label[for] binds the native field, error dashes the lane border and wires aria-invalid + aria-describedby on the input to the “! message”
line.
props
Props
Props extend the native HTML input attributes — minus size and color, whose names the eight-axis surface owns (the §1 native collision rule); the entries below are color-picker-specific additions, and everything else rides the rest object through to the field.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
value bind | string | — | The committed color string — the ONE value model every surface flows through (field typing, native swatch pick, editor drag, bind write). Notation follows format; oklch is the conversion hub, so round-trips stay exact. |
| format | ColorFormat | — | Input and output notation: 'hex' | 'hsl' | 'oklch'. A format switch re-emits the SAME color in the new notation; pasted text in any notation parses and commits canonically. |
| name | string | — | Form field name — the native input[type=text] submits its string under it through its own FormData lane (the swatch never carries a second name lane). |
| label | string | — | Renders a same-row label[for] bound to the native field — a REAL label binding, not an aria-only name. |
| error | string | — | Adds invalid state and an associated message: the lane border dashes, aria-invalid lands on the field, and the describedby chain points field AND swatch at the "! message" line (the input.svelte error law). |
| disabled | boolean | false | The platform disabled semantics on every native control at once — field, swatch and chevron. |
| variant | ColorPickerSurfaceVariant | 'auto' · Own default, not ambient | The editor panel's surface treatment: 'solid' | 'acrylic' | 'auto' — the floating-surface paint has a declared own ('auto') and no axis yet (the dialog/sheet precedent): explicit ?? 'auto', never context. The panel rides the terminal bezel law either way. |
| showSwatch | boolean | true | Mounts the native input[type=color] swatch riding the generated COLOR LAW face (the conic well chip — laws/color.ts, the 4th mounting surface). Clicking it opens the ENGINE picker: the native path every input mode gets. false removes that path entirely. |
| showValue | boolean | true | Shows the value text in the lane; false keeps the native field as the sr-only value carrier — label, name and ARIA intact (the native contract never drops). |
| lane | Snippet<[lane: { text: string; open: boolean; disabled: boolean }]> | — | A custom lane beside the swatch — your content owns the visible spot (it sees { text, open, disabled }) while the native field goes sr-only: label[for], name and the ARIA wiring survive (the input.svelte slot law). |
| aria-invalid | 'true' | 'false' | undefined | — | Caller-supplied validation state — used only when the error wiring is absent (the merge: the error law outranks, but never drops, the caller relation). |
| aria-describedby | string | undefined | — | Caller-supplied describedby — merged under the same law: the error wiring wins when present, the caller relation survives when not. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on color-picker
The family is the COLOR INSTRUMENT: its anatomy rides the density ruler's ambient channels (the well IS the floor equation), its chrome is deliberately hue-neutral, and the §11 carriers it stamps have zero family readers — size/color/radius/shape/elevation/motion are supply-only with grep receipts (the old page's CONSUMES claim retired under probe). Density is consumed through the ambient scope, theme splits three measured ways. Census: batch A (migration-census.md).
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | SUPPLY-ONLY for the lane anatomy — the root stamps --jx-size-effective plus an inline font-size, and the stamp reaches only unstyled flow under the root: the field's type is density-channel-anchored (--jx-text), the well floor is --jx-color-lane, and the editor's parts are rem-fixed (measured: a 14px stamp moves the root's own font-size and nothing on the lane — 13px field, 46px well). The §1 collision applies: the native input never receives a size attribute. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | ambient scope | SUPPLY-ONLY — stamps --jx-shape-effective + --jx-radius-factor-effective; zero readers (grep receipt: zero carrier reads in ui/color-picker/). The lane is a square-edged control shell (borderRadius 0 — the input-law field shell), and the swatch's roundness is the COLOR LAW's own 50% circle, not this axis. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-radius-effective; zero readers (the same grep receipt). The lane's shell is square by the input law and the swatch rides its generated circle; nothing in the family computes the §3 concentric corner. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'xs' | '2xs' | 'sm' | 'default' | 'lg' | 'auto' | number | ambient scope | CONSUMED through the ambient scope (the no-opinion slot — no provider, no explicit prop stamps nothing, and the ambient css scope channel keeps flowing): the well IS the density ruler. min-block-size rides --jx-color-lane = max(--jx-hit, --jx-icon + 2×--jx-inset + 2px), the type rides --jx-text — measured 34/36/46/58px well floors · 11/12/13/15px type across xs/sm/default/lg. The NUMBER lane is inert (the coefficient composes at the :root rung scopes; the wrapper never re-declares — measured: a 3× coefficient leaves 46/13 unmoved). Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | ambient scope | SUPPLY-ONLY — this family IS the color instrument, and the instrument is deliberately hue-neutral: the axis stamps --jx-color-effective (named → the §12 var indirection; a number → hue degrees through the oklch formula — the chip grammar, measured verbatim on the root's style attr) and zero family css or atoms read it (grep receipt: zero --jx-color-effective readers in ui/color-picker/ — measured: a stamped carrier leaves border, ground and well shadow unmoved). The paint a caller wants hue-injected goes through the jx-hue-* utilities on the surrounding tree, exactly as on chip. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | ambient scope | PARTIAL re-theme — measured, THREE voice classes. FLIPS (the raw token layer, substitution at the element): the field's caret (--primary), the swatch's border (--border), the well's shadow (--shadow-well — the black inset goes white), the focus ring (--ring) and the editor panel's terminal fill (--terminal). STAYS FROZEN: the trigger chrome's border/ink/ground (the stylex aliases --jx-border/--jx-foreground/--jx-background, substituted at :root — measured identical under the island). THEME-NEUTRAL: the instrument's color-space constants — the conic swatch, the pad's white/black overlays, the dots' #000 halo (they must read on EVERY hue). light and system stamp nothing — tree inheritance. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | ambient scope | SUPPLY-ONLY — the well's shadow is the input-law well tier (--shadow-well resting, --shadow-well-hover on hover — intensity, not tier), raw tokens outside this lane; the carrier stamps --jx-elevation-effective and zero family css consumes it (grep receipt). The panel's ::after shadow layer rides the terminal bezel law. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it (grep receipt). The lane's own transitions ride the site motion tokens (--motion-150 / --motion-ease-out, with the reduced-motion kill); the editor panel's enter/exit is the surface-motion kernel (the popover wiring law) — behavior, not this lane. Number unit: coefficient. |
Deviations, cited: the adoption is the census batch A row (explicit-props W3 — the 16 native families join the eight-axis surface; openspec/changes/explicit-props/research/migration-census.md). The §1 collision rule: the interface Omits 'size' | 'color', the native input never receives an axis name. The variant prop is the surface slot's OWN literal ('auto' — no axis yet, the dialog/sheet precedent), not a collision.
color-picker · the ruler and the island
The number lanes stamp and re-base nothing here — the declaring element for every channel the family reads is the ambient scope (:root), the wrapper never re-declares (the declaring-element law). Measure the well floor, not the coefficient: 34 / 36 / 46 / 58px across xs / sm / default / lg — the ruler equation max(--jx-hit, --jx-icon + 2×--jx-inset + 2px).
<script lang="ts">
import ColorPicker from '@ui/color-picker/index';
</script>
<div class={cx(rt.gridSm2, rt.wFull)}>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>density default — the ruler's floor equation (46px well, 13px type)</span>
<ColorPicker label="density default well" value="#007924" />
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>density="lg" — the touch tier steps the whole lane (58px, 15px)</span>
<ColorPicker label="density lg well" value="#007924" density="lg" />
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>theme="dark" — the caret, swatch border and well shadow flip; the trigger chrome stays frozen</span>
<ColorPicker label="dark well" value="#007924" theme="dark" />
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>color="error" — stamped, unread: the instrument stays hue-neutral (the lane paints nothing semantic)</span>
<ColorPicker label="color carrier well" value="#007924" color="error" />
</div>
</div>
<p class={cx(rt.mt8, rt.note12, rt.inkMuted70)}>
The number lanes stamp and re-base nothing here — the declaring element for every
channel the family reads is the ambient scope (:root), the wrapper never re-declares
(the declaring-element law). Measure the well floor, not the coefficient:
34 / 36 / 46 / 58px across xs / sm / default / lg — the ruler equation
max(--jx-hit, --jx-icon + 2×--jx-inset + 2px).
</p><script lang="ts">
import ColorPicker from '@ui/color-picker/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<ColorPicker label="responsive well" bind:value={c} density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} />color-picker · query()
Media keys are min-width: below 40rem the base applies — the small rung, the pointer-lane well (36px, 12px type); at 40rem and wider the lg case wins and the well steps to the touch tier (58px, 15px — measured). Resize across 40rem.
<script lang="ts">
import ColorPicker from '@ui/color-picker/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<ColorPicker label="responsive well" bind:value={c} density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} />| Token | Default | Source |
|---|---|---|
| --jx-color-lane | max(--jx-hit, --jx-icon + 2×--jx-inset + 2px) — 34 / 36 / 46 / 58px across xs / sm / default / lg (measured) | component |
| --jx-text | 11 / 12 / 13 / 15px across xs / sm / default / lg | density |
| --jx-line | 16 / 18 / 20 / 24px across xs / sm / default / lg | density |
| --jx-color-picker-hue | runtime hue angle (the committed value) | component |
a11y
Accessibility
The value surface is a native input[type=text] (label, focus, selection); the swatch is a native input[type=color]; the popover supplies Escape and light-dismiss behavior.
Keyboard
| Key | Action |
|---|---|
| Type + Enter | Edit the value in the native field; parsed text commits, invalid drafts revert |
| Click swatch | Open the engine color picker (native input[type=color]) |
| Enter / Space on chevron | Open the editor popover (native popover=auto) |
| Escape | Close the popover and restore field focus |
| Tab | Move through the lane controls and picker fields |
| Keyboard-only picking (honest limits) | The SV pad and hue rail are pointer-only decorative aids (aria-hidden); keyboard picking rides the value field (any notation), the format select and the Swatches grid — and the engine picker only while the native swatch is mounted (showSwatch=false removes that path entirely). |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-invalid | true | Set on the field when error is present |
| aria-describedby | {id}-error | Points the field — and the native swatch — at the “! message” line when invalid |
| aria-expanded | true | false | On the chevron; reflects popover visibility |
| aria-haspopup | true | On the chevron; the generic promise — the panel opens as role=group, not a dialog |
| aria-controls | {id}-panel | Connects the chevron to its panel |