jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — color-picker

$npx jixoai-ui add color-picker

https://ui.jixoai.com/r/color-picker.json

native input[type=text] field native input[type=color] swatch SV pad + hue bar hex / hsl / oklch Eye Dropper API invalid drafts revert zero deps · Svelte 5 runes

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.

ColorPicker usage
<!-- 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.

color-picker-usage.svelte svelte
<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

color-picker-types.svelte svelte
<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

bound value: #007924
notation follows format
showSwatch / showValue shape the trigger
color-picker-catalog-demo.svelte svelte
<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

color-picker-error-demo.svelte svelte
<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

PropertyTypeDefaultDescription
value bindstring—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 booleanfalseThe platform disabled semantics on every native control at once — field, swatch and chevron.
variant ColorPickerSurfaceVariant'auto' · Own default, not ambientThe 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 booleantrueMounts 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 booleantrueShows 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

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity 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).

PropertyTypeDefaultDescription
size 'small' | 'medium' | 'large' | 'auto' | numberambient scopeSUPPLY-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 scopeSUPPLY-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' | numberambient scopeSUPPLY-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' | numberambient scopeCONSUMED 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 | stringambient scopeSUPPLY-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 scopePARTIAL 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' | numberambient scopeSUPPLY-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' | numberambient scopeSUPPLY-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

density default — the ruler's floor equation (46px well, 13px type)
density="lg" — the touch tier steps the whole lane (58px, 15px)
theme="dark" — the caret, swatch border and well shadow flip; the trigger chrome stays frozen
color="error" — stamped, unread: the instrument stays hue-neutral (the lane paints nothing semantic)

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).

color-picker-axes.svelte svelte
<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>
one real query() case
<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.

color-picker-query-demo.svelte svelte
<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')} />
TokenDefaultSource
--jx-color-lanemax(--jx-hit, --jx-icon + 2×--jx-inset + 2px) — 34 / 36 / 46 / 58px across xs / sm / default / lg (measured)component
--jx-text11 / 12 / 13 / 15px across xs / sm / default / lgdensity
--jx-line16 / 18 / 20 / 24px across xs / sm / default / lgdensity
--jx-color-picker-hueruntime 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

KeyAction
Type + EnterEdit the value in the native field; parsed text commits, invalid drafts revert
Click swatchOpen the engine color picker (native input[type=color])
Enter / Space on chevronOpen the editor popover (native popover=auto)
EscapeClose the popover and restore field focus
TabMove 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

AttributeValueDescription
aria-invalidtrueSet on the field when error is present
aria-describedby{id}-errorPoints the field — and the native swatch — at the “! message” line when invalid
aria-expandedtrue | falseOn the chevron; reflects popover visibility
aria-haspopuptrueOn the chevron; the generic promise — the panel opens as role=group, not a dialog
aria-controls{id}-panelConnects the chevron to its panel
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h