DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Color Picker

Figma-style color picker — saturation/value canvas, hue and alpha sliders, eyedropper, HEX/RGB/HSL/HSB inputs, swatch libraries, recents, a standalone swatch picker, and a WCAG contrast checker with live boundary curves. Form-ready, sheet presentation on phones, light/dark, RTL.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { ColorPicker, ColorPickerPanel, ColorSwatchPicker } from "@tessinaui/ui";
{/* Inline panel */}
<ColorPickerPanel defaultValue="#711DC8" onValueChange={setColor} />

{/* Button-triggered popover (bottom sheet below md) */}
<ColorPicker value={color} onValueChange={setColor} label="Brand color" />

{/* Preset-palette picker — no canvas at all */}
<ColorSwatchPicker items={palette} value={color} onValueChange={setColor} />

value accepts hex (#RRGGBB/#RRGGBBAA) plus rgb(), hsl() and hsb() CSS strings. Named colors ("red") are rejected — parseColorString returns null for them, matching React Aria's contract. Emitted values are always uppercase hex.

Examples

Default

The popover trigger with quick swatches.

Commit vs live changes

onValueChange fires per drag frame; onValueCommit fires once per finished interaction — with the original-vs-current preview bar.

Color models

HEX, RGB, HSL and HSB, controlled via model + onModelChange.

In a form

name submits the hex through a hidden input, label names the trigger and panel, required stars it, and errorMessage swaps in with the error ring.

Read-only and disabled

On color

Mobile sheet

Below md the popup presents as a bottom sheet; resize to see the switch.

Swatch-only picker

ColorSwatchPicker — grid and row layouts, a none tile, check indicators, and one-Tab-stop keyboard navigation.

Progressive disclosure

Presets first; the rainbow tile escalates to the full panel.

Recents and saved swatches

The recent-colors row tracks commits; the "+" tile saves into a consumer-owned palette.

Sliders only

showArea={false} — the compact panel.

Trigger variants

The field-like trigger and the bare triggerMode="swatch" color well.

Libraries

Named swatch groups render in a second tab.

Contrast check

The badge shows the live ratio; the solid boundary marks the target threshold and showBothBoundaries adds the other level dashed.

Controlled trigger

API Reference

ColorPickerPanel

PropTypeDefaultDescription
valuestring—Controlled value — hex, rgb(), hsl() or hsb() string
defaultValuestring"#711DC8"Uncontrolled initial value
onValueChange(value: string) => void—Every change (drag frames included); 8-digit hex only when showAlpha and alpha < 1
onValueCommit(value: string) => void—Once per finished interaction: release, keyboard step, input commit, swatch/eyedropper pick
showAlphabooleantrueAlpha rail + opacity input
showEyedropperbooleantrueEyedropper button (auto-hidden without browser support)
showInputsbooleantrueModel select + channel inputs row
showAreabooleantrueSV canvas; false is the sliders-only panel
defaultModel"hex" | "rgb" | "hsl" | "hsb""hex"Initial input model
model / onModelChange——Controlled input model
labelstring—Visible heading; names the panel group
showPreviewbooleanfalseOriginal-vs-current bar; the original half restores on click
namestring—Hidden input carrying the current hex for native forms
requiredbooleanfalseStars the label; validation is the consumer's
readOnlybooleanfalseFocusable and announced, but the value cannot change
swatchesColorSwatchItem[]—Quick swatches under the inputs
swatchesLabelstring"Swatches"Heading above the quick swatches
indicator"ring" | "check""ring"Selected-swatch marker
onSwatchAdd(value: string) => void—Renders the "+" save tile
recentColors / defaultRecentColorsstring[]—Recent-colors row (auto-appended on commit, deduped, capped at 8)
onRecentColorsChange(colors: string[]) => void—Fires with the updated recents
recentColorsLabelstring"Recent"Heading above the recents
librariesColorLibrary[]—Named groups — presence renders the Custom | Libraries tabs
contrastCheckContrastCheckOptions—Ratio badge + boundary curve(s)
size"sm" | "md""md"Panel width — 280px / 320px
rounded"none" | "sm" | "md" | "lg" | "full""md"Corner radius (cascades to every internal control)
disabledbooleanfalseDisables all interaction
dir"ltr" | "rtl""ltr"Chrome direction (color geometry stays LTR)
onClose() => void—Presence renders the × header button

ColorPicker (popover trigger)

Extends the panel's functional props, plus:

PropTypeDefaultDescription
placeholderstring"Pick a color"Trigger label when no value renders
labelstring"Color"Stable accessible trigger name (leads the announced value) + sheet title
triggerVariantButtonProps["variant"]"outline"Trigger button variant
triggerSizeButtonProps["size"]"sm"Trigger button size
triggerMode"field" | "swatch""field"Field-like trigger, or the bare color well
tone"default" | "on-color""default"Legible on tinted/dark plates
intent"none" | "error" | "warning" | "success" | "info""none"Semantic border + ring on the trigger
errorMessagestring—Message under the trigger; implies intent="error"
closeOnSelectbooleanfalseDismiss the popup on swatch pick
mobilePresentation"sheet" | "popover""sheet"Bottom sheet below md, or always the popover
side / alignpositioning"bottom" / "start"Popover placement
open / defaultOpen / onOpenChange——Popover open state

ref and forwarded HTML attributes (data-*, handlers) land on the TRIGGER — the persistent element; the panel unmounts while closed.

ColorSwatchPicker

PropTypeDefaultDescription
itemsColorSwatchItem[]—The palette
value / defaultValuestring—Selected hex; "" means none
onValueChange(value: string) => void—Selection change
layout"grid" | "row""grid"Wrapping grid or one scrollable line
columnsnumber8Grid column count
size"sm" | "md" | "lg""md"Tile size — 20 / 24 / 32px
rounded"none" | "sm" | "md" | "lg" | "full""md"Tile radius; full = circles
indicator"ring" | "check""ring"Selected marker
allowNonebooleanfalsePrepends the no-color slash tile
onCustomClick() => void—Renders the trailing rainbow custom tile
labelstring"Color swatches"Group name for assistive tech
namestring—Hidden input for native forms
disabled / readOnlybooleanfalseStates

ColorPickerSwatch

PropTypeDefaultDescription
colorstring—Hex color of the tile
namestring—Accessible name (announced instead of the hex)
selectedbooleanfalseSelection marker
indicator"ring" | "check""ring"Marker style
onSelect(value: string) => void—Click handler
size"sm" | "md" | "lg""md"Tile size
rounded"none" | "sm" | "md" | "lg" | "full""md"Tile radius

Skeletons

ColorPickerSkeleton mirrors the closed trigger (size, rounded, triggerVariant, triggerSize). ColorPickerPanelSkeleton mirrors the open panel (showArea, preview, recents, showAlpha, showEyedropper, showInputs, swatches, libraries, contrast, closeable, dir). ColorSwatchPickerSkeleton mirrors the swatch picker (items, layout, columns, size, rounded).

Types

interface ColorSwatchItem {
  value: string; // "#RRGGBB" | "#RRGGBBAA"
  name?: string; // accessible name
}

interface ColorLibrary {
  name: string; // group heading
  colors: ColorSwatchItem[];
}

interface ContrastCheckOptions {
  against: string;              // comparison color (hex)
  target?: "AA" | "AAA";        // default "AA"
  largeText?: boolean;          // 3.0 / 4.5 thresholds instead of 4.5 / 7.0
  showBothBoundaries?: boolean; // dashed second curve for the other level
}

The color utilities hexToHsva, hsvaToHex, parseColorString, and contrastRatio are also exported for app-side use.

Accessibility

The canvas and rails follow the WAI-ARIA slider pattern (the canvas is a 2D slider announcing both channels through aria-valuetext); swatch sets are roving-tabindex toggle groups — one Tab stop, aria-pressed selection.

Keyboard

KeyWhereAction
Tab / Shift+TabeverywhereMove between the canvas, rails, inputs and swatch groups
Arrowscanvas±1% saturation (←→) / brightness (↑↓); Shift ±10%
Arrowsrails±1 step; Shift ±10; hue wraps 359° → 0°
PageUp / PageDowncanvas, railsPage step — brightness 10%, hue 15°, opacity 10%
Home / Endcanvas, railsJump to the extremes (hue pins 0° / 360°, no wrap)
Arrows / Home / Endswatch groupsMove between tiles (grid rows move by columns)
Enter / Spaceswatch tilesSelect
Escpopover / sheetClose and return focus to the trigger
  • The contrast badge announces its ratio after the value settles (~150ms), never per drag frame.
  • readOnly keeps every control focusable and announced with aria-readonly="true".
  • The trigger's accessible name leads with label ("Color: #711DC8"), so multiple pickers stay distinguishable.

AT matrix

Accessibility tree (Chromium, computed names/roles/states): verified across every surface — trigger, error state, sheet, sliders, swatch groups, read-only and the contrast announcer. What that pass established:

SurfaceAnnounced as
Triggerbutton "Brand color (required): #711DC8", haspopup="dialog", expandable
Errortrigger gains description=; the message is a polite live region; the trigger node survives the error appearing/clearing (no focus loss)
Sheetdialog "Brand color" + one level-2 heading — the panel does not repeat the title
Slidersnamed Color / Hue / Opacity, each with aria-valuetext ("Saturation 86%, Brightness 78%", "Hue 269°", "100% opacity")
Swatchesgroup "Project color" → button "Purple" pressed, described by its hex — the name is never repeated
Read-onlytext fields readonly, sliders aria-readonly and still focusable, swatches announce as unavailable (aria-disabled — aria-readonly is illegal on role="button")
Contrastone polite region: "Contrast ratio 7.74 to 1, passes AA", announced on settle

VoiceOver listening: NOT yet recorded. The tree pass above verifies what is announced, not how it sounds in sequence — only a human (or a scripted VoiceOver session) can confirm that. NVDA and JAWS: UNTESTED — no Windows host exists for this project. Both gaps keep the badge at beta (QA flow §5.6).

Notes

  • Anatomy (2026-09-03 redesign) — the SV canvas is a full-width SQUARE (equal travel per pixel on both axes; macOS's NSColorPanel ships the same 1:1), the value inputs are ONE bordered group split by hairline seams (model select · value · opacity, with the % inside the box), the eyedropper is an outlined square beside the rails, and the swatch heading carries the "+ Add" action at its end. The row runs on the input family's own control scale: sm controls in the md panel, xs in the sm panel.
  • Value model — external strings in, uppercase hex out; the picker keeps an internal HSVA state so hue survives passing through black, white, and gray. HSB inputs map 1:1 onto that state.
  • RTL & color geometry — panel chrome mirrors with dir="rtl"; the canvas and rails deliberately stay LTR (React Aria mirrors them instead — recorded divergence: color geometry is a cross-tool convention).
  • Contrast checking — translucent colors are measured as rendered, composited over the comparison color. Boundary curves are recomputed per hue via binary search on the WCAG luminance, which is monotonic in brightness.
  • Browser support — the eyedropper uses the native EyeDropper API (Chromium only) and hides itself elsewhere.
  • Mobile — the popup becomes a swipe-dismissable bottom sheet below md; inputs and thumbs expand to 44px touch targets on small screens (WCAG 2.5.5).
  • Forms — the hidden input lives on the trigger side of the popover variant, so the value submits while the popup is closed.
CollapsibleCombobox

On this page

PlaygroundInstallationUsageExamplesDefaultCommit vs live changesColor modelsIn a formRead-only and disabledOn colorMobile sheetSwatch-only pickerProgressive disclosureRecents and saved swatchesSliders onlyTrigger variantsLibrariesContrast checkControlled triggerAPI ReferenceColorPickerPanelColorPicker (popover trigger)ColorSwatchPickerColorPickerSwatchSkeletonsTypesAccessibilityKeyboardAT matrixNotes