sveltelean Primitives
Versionv0.2.0 GitHub

Example

Accent color
<label for="color-field-accent">Accent color</label>
<input
	id="color-field-accent"
	type="color"
	name="accent"
	value="#2f6fde"
	data-slean="color-field"
/>
Tier 0: the Svelte source adds the stylesheet import and a binding. The picker is the browser’s, and in some browsers the operating system’s color panel. Everything here works with page JavaScript disabled.

Why this implementation exists

A custom color picker is a large piece of interface: a saturation plane, a hue slider, text fields, and keyboard support for each. The platform ships one behind <input type="color">, keyboard-operable, with the value in one fixed format.

Svelte Lean draws the control as a swatch in a bordered box, through the swatch pseudo-elements, and leaves the picker to the browser.

The browser owns

  • the color picker (in some browsers the operating system’s color panel)
  • the value, always a lowercase #rrggbb string
  • Enter and Space to open the picker, and the picker’s own keyboard
  • form participation and the label association

Svelte Lean owns

  • color-field.css: the bordered box and the swatch, through the prefixed swatch pseudo-elements
  • the contract
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
stylesheets
import '@svelte-lean/styles/color-field.css';

Write value as #rrggbb. The value read back is always lowercase hex, whatever the picker showed.

The swatch shows the color, not its code. When the reader needs the code, bind the value and show it in an <output>.

Anatomy

PartElementdata-slean-partRequiredNotes
root<input type="color" data-slean="color-field" value="#2f6fde">–yesA visible label: the swatch shows the value, not the purpose. The value is #rrggbb.
swatch::-webkit-color-swatch, ::-moz-color-swatch–styles onlyThe browser’s swatch; the stylesheet rounds it and gives it a border.

Runtime profile

Tier 0: the field has no behavior module, and the Vite plugin maps color-field to no module. The picker and the value are the browser’s.

Accessibility contract

  • A visible label through <label for>: the swatch shows the value, not the purpose.
  • One tab stop; Enter or Space opens the picker.
  • The picker’s keyboard and its announcements belong to the browser or the operating system.

Keyboard

KeyWhenResult
Tab/Shift+TabanywhereMoves focus to and from the swatch (native)
Enter/Spacefocus on the swatchOpens the picker, which has its own keyboard (native)

Platform features

FeatureBaselineOutside the target
<input type="color">Widely availableNot applicable; the picker differs between browsers
::-webkit-color-swatch, ::-moz-color-swatchPrefixed, non-standardThe browser’s default swatch inside the styled box
alpha, colorspaceSafari 27 implements both; Chrome 153 neitherNot used: the value stays #rrggbb in every browser

Without JavaScript

Fully functional: picking a color and submitting it with a form need no script.

Server rendering

The value attribute renders the initial color.

Before hydration

Nothing is attached; the field works before and after hydration alike.

Styling

color-field.css removes the native appearance, draws a bordered box at the control height and rounds the swatch through ::-webkit-color-swatch and ::-moz-color-swatch, each in its own rule because a selector list with an unknown pseudo-element is dropped whole. Under forced colors the swatch keeps its color, since it is the value. The tokens it reads:

TokenDefault (light)Applies to
--slean-control-height-md2.25reminline-size, block-size
--slean-space-10.25rempadding
--slean-control-bordervar(--slean-border-strong)border
--slean-radius-md0.625remborder-radius
--slean-control-bgvar(--slean-surface)background
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-bordervar(--slean-neutral-6)border
--slean-radius-sm0.375remborder-radius
--slean-control-border-hovervar(--slean-accent)border-color
--slean-control-border-focusvar(--slean-accent)border-color
--slean-control-ring0 0 0 3px color-mix(in oklch, var(--slean-accent) 22%, transparent)box-shadow
--slean-warningoklch(76% 0.16 80)border-color
--slean-control-ring-warning0 0 0 3px color-mix(in oklch, var(--slean-warning) 28%, transparent)box-shadow
--slean-dangeroklch(55% 0.2 25)border-color
--slean-control-ring-danger0 0 0 3px color-mix(in oklch, var(--slean-danger) 22%, transparent)box-shadow
--slean-control-border-disabledvar(--slean-border)border-color
--slean-control-bg-disabledvar(--slean-muted)background-color
--slean-fg-mutedvar(--slean-neutral-11)color

State selectors the stylesheet targets, all from the platform or ARIA: ::-moz-color-swatch, ::-webkit-color-swatch, ::-webkit-color-swatch-wrapper, :disabled, :focus-visible, :hover, :user-invalid, [aria-invalid="true"].

Variant attributes: data-status (warning, error).

Controlled integration

Bind the value to show it, store it or apply it. It is always a lowercase #rrggbb string.

accent.svelte
<script lang="ts">
	let accent = $state('#2f6fde');
</script>

<label for="color-field-accent">Accent color</label>
<input id="color-field-accent" type="color" name="accent" bind:value={accent}
	data-slean="color-field" />
<!-- The value is always lowercase #rrggbb. -->
<output for="color-field-accent">{accent}</output>

Compatibility notes

The picker differs between browsers. The HTML standard adds alpha and colorspace attributes: Safari 27 implements both, and with either one the value becomes a CSS color function instead of hex; Chrome 153 implements neither. The contract does not use them, so the value stays #rrggbb everywhere.

Testing

Source