Input Elements

Color Picker

Import ColorPicker from @leon8ix/jhana-ui/color. Its closed field uses the input surface and interaction styles, with a swatch and the current mode's channel values. It keeps the focused input appearance while the popup is open. Click it to open a popup with a slider and numeric input for each channel. Each channel label has a base-color swatch. RGB uses 0-255; CMYK and grayscale brightness use percentages. Brightness runs from black at 0% through middle gray at 50% to white at 100%. The bound value retains the mode and its channels; use colorToHex for a CSS preview.

Preview: #4080e0

{
  "mode": "rgb",
  "r": 64,
  "g": 128,
  "b": 224
}

ColorPickerDemo.svelte

<script lang="ts">
	import { ColorPicker, colorToHex, type ColorValue } from '@leon8ix/jhana-ui/color';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';

	let color = $state<ColorValue>({ mode: 'rgb', r: 64, g: 128, b: 224 });
</script>

<div class="stack">
	<ColorPicker dict={dictEn} bind:value={color} />
	<p class="selectable">Preview: <code>{colorToHex(color)}</code></p>
	<pre class="selectable">{JSON.stringify(color, null, 2)}</pre>
</div>

Label Actions

Use the same labelEnd snippet as NumberInput and Field to place a reset button or other custom content beside the label. Change the color to reveal the reset button. The app owns the default value and the action's visibility and disabled state. ButtonRevert keeps its space while hidden, and the reset action works independently of opening the picker.

ColorPickerLabelDemo.svelte

<script lang="ts">
	import { ButtonRevert } from '@leon8ix/jhana-ui';
	import { tooltip } from '@leon8ix/jhana-ui/actions';
	import { ColorPicker, type ColorValue } from '@leon8ix/jhana-ui/color';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';

	const defaultColor = { mode: 'rgb', r: 64, g: 128, b: 224 } satisfies ColorValue;
	let color = $state<ColorValue>({ ...defaultColor });
	const isDefault = $derived(
		color.mode === 'rgb' && color.r === defaultColor.r && color.g === defaultColor.g && color.b === defaultColor.b,
	);
</script>

<ColorPicker dict={dictEn} label="Print color" bind:value={color}>
	{#snippet labelEnd()}
		<ButtonRevert
			type="button"
			name="Reset print color"
			hidden={isDefault}
			onclick={() => (color = { ...defaultColor })}
			{@attach tooltip('Restore the default print color.', { delay: 'long' })}
		/>
	{/snippet}
</ColorPicker>

Dictionaries

The dict prop is required; the picker has no default language. Import dictEn from @leon8ix/jhana-ui/color/dict/en or dictDe from /color/dict/de, or supply your app's own ColorPickerDict. The /color entry does not import either dictionary; unused languages stay out of the bundle. This example explicitly imports both to switch languages. All built-in labels, including accessible names and the default field label, follow dict. Pass label and hint for app-specific text.

LocalizedColorPickerDemo.svelte

<script lang="ts">
	import { Radio } from '@leon8ix/jhana-ui';
	import { ColorPicker, type ColorValue } from '@leon8ix/jhana-ui/color';
	import { dictDe } from '@leon8ix/jhana-ui/color/dict/de';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';

	let language = $state('de');
	let color = $state<ColorValue>({ mode: 'rgb', r: 64, g: 128, b: 224, alpha: 0.6 });
</script>

<div class="stack">
	<div class="input-group boxed" role="radiogroup" aria-label="Language">
		<Radio name="color-language" label="English" value="en" bind:group={language} />
		<Radio name="color-language" label="Deutsch" value="de" bind:group={language} />
	</div>
	<ColorPicker dict={language === 'de' ? dictDe : dictEn} opacity bind:value={color} />
</div>

Available Modes

Pass modes to choose the available modes and their order. With one mode the selector is hidden. The list must contain at least one mode. If the current mode becomes unavailable, the color converts to the first allowed mode, including when the consumer assigns a value from another mode.

0% is black; 100% is white.

RestrictedColorPickerDemo.svelte

<script lang="ts">
	import { ColorPicker, type ColorValue } from '@leon8ix/jhana-ui/color';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';

	let printColor = $state<ColorValue>({ mode: 'cmyk', c: 70, m: 25, y: 0, k: 10 });
	let monochrome = $state<ColorValue>({ mode: 'grayscale', brightness: 50 });
</script>

<div class="stack">
	<ColorPicker dict={dictEn} label="Print color" modes={['cmyk', 'grayscale']} bind:value={printColor} />
	<ColorPicker
		dict={dictEn}
		label="Monochrome"
		modes={['grayscale']}
		bind:value={monochrome}
		hint="0% is black; 100% is white."
	/>
</div>

Optional Opacity

Opacity is off by default and alpha is omitted from the bound value. Add opacity to show its slider and numeric input. The control displays 0–100%; the value includes alpha from 0 (transparent) to 1 (opaque). Checkerboard swatches show transparency. Alpha is preserved when changing color modes.

{
  "mode": "rgb",
  "r": 64,
  "g": 128,
  "b": 224,
  "alpha": 0.6
}

OpacityColorPickerDemo.svelte

<script lang="ts">
	import { ColorPicker, type ColorValue } from '@leon8ix/jhana-ui/color';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';

	let color = $state<ColorValue>({ mode: 'rgb', r: 64, g: 128, b: 224, alpha: 0.6 });
</script>

<div class="stack">
	<ColorPicker dict={dictEn} label="Overlay color" opacity bind:value={color} />
	<pre class="selectable">{JSON.stringify(color, null, 2)}</pre>
</div>

Read-only

Pass readonly to let users open the popup and inspect its values without editing. The mode selector, sliders and steppers are disabled, and numeric inputs are read-only. Changing readonly while the popup is open locks or unlocks editing without closing it. Use disabled to prevent opening the popup entirely.

ReadonlyColorPickerDemo.svelte

<script lang="ts">
	import { ColorPicker } from '@leon8ix/jhana-ui/color';
	import { dictEn } from '@leon8ix/jhana-ui/color/dict/en';
</script>

<ColorPicker
	dict={dictEn}
	label="Read-only color"
	value={{ mode: 'cmyk', c: 20, m: 40, y: 0, k: 15, alpha: 0.6 }}
	opacity
	readonly
/>

Editing and Conversion

Click the swatch or focus it and press Enter or Space to open the popup. Each channel has a native slider and a matching numeric input. Opening focuses the first numeric input; Tab moves between numeric inputs and skips the sliders. All sliders use whole-number steps. Numeric inputs use their default step of 1 and preserve typed decimals in every channel. Escape, outside clicks and clicking the field again dismiss the popup. Edits apply immediately, and the swatches in the input field and beside the mode selector preview the resulting color. The shaded sliders use small filled dots that grow into rings on hover, drag or keyboard focus. Each handle uses whichever of the theme's dark and light tones contrasts better with the track at its current position. The opacity handle accounts for the checkerboard too.

CMYK channels remain intact while editing them. Previews and mode changes use approximate, uncalibrated RGB/CMYK conversion. Print profiles and ink limits belong to the consumer app. Switching to Grayscale discards chroma; switching back produces a neutral color. Conversions can round channel values.

Use bind:value for state, or onchange for a callback receiving the new ColorValue on each user edit. External value, modes and opacity changes do not call onchange. The class and style props target the field wrapper. Native button attributes target the trigger. Use label and hint for its visible name and instructions.