Skip to content

Advanced Topics

The <color-input> component is built with accessibility as a core requirement, following WAI-ARIA Authoring Practices Guide (APG) patterns.

All functionality is accessible via keyboard:

Opening/Closing:

  • Enter or Space on the trigger button - Opens the picker
  • Escape - Closes the picker and returns focus to the trigger
  • Tab - Navigates between controls when open

Adjusting Colors:

  • Arrow Up/Down - Increment/decrement value by 1 unit
  • Shift + Arrow Up/Down - Increment/decrement by 10 units
  • Home - Set to minimum value
  • End - Set to maximum value
  • Tab - Move between channel controls

Try keyboard navigation: Tab to focus, Enter to open, Arrow keys to adjust sliders, Escape to close.

Value
oklch(70% 0.2 240)
Color Space
oklch
Gamut
Contrast Color
View Code
<color-input
value="oklch(70% 0.2 240)"
colorspace="oklch"
theme="auto"
></color-input>

The component uses semantic HTML with proper ARIA attributes:

  • Semantic Elements: Native <button>, <input type="range">, <input type="number">
  • ARIA Labels: aria-label for control purpose (e.g., “Lightness slider”)
  • Live Regions: aria-live="polite" for value announcements
  • Range Properties: aria-valuenow, aria-valuemin, aria-valuemax

Focus Rings: All interactive elements have visible focus indicators using :focus-visible.

Focus Trap: When the picker is open, Tab cycles through controls within the popover. Escape closes and returns focus to the trigger.

Focus Restoration: Focus returns to the trigger button when the picker closes.

The contrastColor property provides the recommended text color for maximum contrast:

const picker = document.querySelector('color-input');
picker.addEventListener('change', (e) => {
const textColor = picker.contrastColor; // "white" or "black"
document.body.style.background = e.detail.value;
document.body.style.color = textColor;
});

The component calculates contrast using WCAG 2.1 formulas for AA/AAA compliance.

Hit Target Sizing:

  • Mobile: ≥44px × 44px
  • Desktop: ≥24px × 24px

Touch Affordances:

  • touch-action: manipulation prevents double-tap zoom
  • Generous padding expands clickable areas
  • No hover-only interactions

Respects prefers-reduced-motion for users sensitive to motion:

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
  • ☐ All features work with keyboard only
  • ☐ Screen reader announces changes properly
  • ☐ Focus indicators are visible
  • ☐ Hit targets meet minimum sizes
  • ☐ Color contrast meets WCAG AA (4.5:1 for text)
  • ☐ Works with reduced motion enabled
  • ☐ Tab order is logical
  • ☐ Popover closes with Escape

The component uses Preact Signals for efficient, fine-grained reactivity:

  • Minimal Re-renders: Only affected parts of the UI update
  • Automatic Dependency Tracking: Signals track dependencies automatically
  • Lightweight: Small runtime overhead

The component uses Shadow DOM for style isolation:

  • No Style Conflicts: Component styles don’t leak to the page
  • CSS Parts API: Customize via ::part() selectors
  • Composability: Use multiple instances without conflicts

Color calculations are powered by colorjs.io:

  • Accurate Conversions: Between all supported color spaces
  • Gamut Mapping: Automatic handling of out-of-gamut colors
  • Standards Compliant: Follows CSS Color Module Level 4

The component is designed for minimal bundle impact:

  • Tree-Shakeable: ES modules allow unused code elimination
  • No Framework Dependencies: Only Preact Signals and colorjs.io
  • Modern Output: Targets ES2020+ for smaller bundles

Efficient Updates:

  • Signals prevent unnecessary re-renders
  • Color conversions are cached
  • Popover uses native Popover API (hardware accelerated)

Lazy Loading:

  • Component registers on import
  • Popover content rendered on-demand

The component uses the native Popover API with intelligent positioning:

The popover automatically positions relative to the trigger button:

  • Respects viewport boundaries
  • Handles safe areas (notches, system UI)
  • Flips when near edges
  • No manual positioning required

Use setAnchor() to position relative to a different element:

const picker = document.querySelector('color-input');
const customAnchor = document.querySelector('#my-anchor');
picker.setAnchor(customAnchor);
picker.show();

The Popover API is required:

  • Chrome: 114+
  • Safari: 17+
  • Firefox: 125+

The component automatically detects which color gamut contains the current color:

The read-only gamut property returns the smallest gamut:

console.log(picker.gamut); // "srgb" | "p3" | "rec2020" | "xyz"

A gamut badge appears in the picker UI showing the current gamut. This helps you understand:

  • srgb: Works on all displays
  • p3: Requires Display P3 or better
  • rec2020: Requires ultra-wide gamut
  • xyz: Beyond Rec2020

The change event includes gamut information:

picker.addEventListener('change', (e) => {
console.log(e.detail.gamut); // "srgb" | "p3" | "rec2020" | "xyz"
});

While the component doesn’t inherit from HTMLInputElement, you can integrate it with forms:

<form id="settings-form">
<label for="theme-color">Theme Color</label>
<color-input id="theme-color" value="oklch(70% 0.2 240)"></color-input>
<input type="hidden" name="theme_color" id="theme-color-input">
</form>
<script>
const picker = document.querySelector('#theme-color');
const hiddenInput = document.querySelector('#theme-color-input');
picker.addEventListener('change', (e) => {
hiddenInput.value = e.detail.value;
});
// Set initial value
hiddenInput.value = picker.value;
</script>

Use properties and methods to control the picker:

const picker = document.querySelector('color-input');
// Read state
console.log(picker.value);
console.log(picker.colorspace);
console.log(picker.gamut);
console.log(picker.contrastColor);
// Update state
picker.value = 'oklch(80% 0.15 180)';
picker.colorspace = 'hsl';
picker.theme = 'dark';
// Control popover
picker.show();
picker.close();