ColorAssembly
Use cases

Building a Color Picker

Create a professional color picker with ColorAssembly

Learn how to build a fully-featured color picker with real-time conversion, accessibility checking, and palette generation.

Overview

We'll build a color picker that:

  • Shows color in multiple formats (HEX, RGB, HSL, OKLAB)
  • Generates harmonies and variations
  • Checks accessibility
  • Provides copy-to-clipboard functionality

Basic Implementation

import { useState, useEffect } from 'react';
import { loadColorAssembly, type ColorAssemblyAPI } from '@cymbol/colorassembly';
 
export function ColorPicker() {
  const [api, setApi] = useState<ColorAssemblyAPI | null>(null);
  const [color, setColor] = useState('#6366f1');
 
  useEffect(() => {
    loadColorAssembly().then(setApi);
  }, []);
 
  if (!api) return <div>Loading...</div>;
 
  const rgb = api.transforms.hexToRgb({ hex: color });
  const hsl = api.transforms.rgbToHsl(rgb);
  const oklch = api.transforms.rgbToOklch(rgb);
 
  return (
    <div className="w-full max-w-md space-y-6 p-6 border rounded-lg">
      {/* Color Preview */}
      <div
        className="w-full h-48 rounded-lg shadow-lg"
        style={{ backgroundColor: color }}
      />
 
      {/* HTML Color Input */}
      <input
        type="color"
        value={color}
        onChange={(e) => setColor(e.target.value)}
        className="w-full h-12 rounded cursor-pointer"
      />
 
      {/* Color Formats */}
      <div className="space-y-3">
        <ColorFormat label="HEX" value={color.toUpperCase()} />
        <ColorFormat label="RGB" value={`rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`} />
        <ColorFormat 
          label="HSL" 
          value={`hsl(${hsl.h.toFixed(0)}°, ${hsl.s.toFixed(0)}%, ${hsl.l.toFixed(0)}%)`} 
        />
        <ColorFormat 
          label="OKLCH" 
          value={`oklch(${oklch.l.toFixed(2)}, ${oklch.c.toFixed(2)}, ${oklch.h.toFixed(0)})`} 
        />
      </div>
    </div>
  );
}
 
function ColorFormat({ label, value }: { label: string; value: string }) {
  const [copied, setCopied] = useState(false);
 
  const copy = () => {
    navigator.clipboard.writeText(value);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
 
  return (
    <div className="flex items-center justify-between p-3 bg-gray-50 rounded">
      <div>
        <div className="text-xs text-gray-500">{label}</div>
        <div className="font-mono text-sm">{value}</div>
      </div>
      <button
        onClick={copy}
        className="px-3 py-1 text-xs border rounded hover:bg-gray-100"
      >
        {copied ? 'Copied!' : 'Copy'}
      </button>
    </div>
  );
}

Advanced Features

Color Harmonies

Add harmony generation to your picker:

function ColorHarmonies({ baseColor, api }: { baseColor: string; api: ColorAssemblyAPI }) {
  const rgb = api.transforms.hexToRgb({ hex: baseColor });
  const harmonies = {
    complementary: api.generate.complementary(rgb),
    triadic: api.generate.triadic(rgb),
    analogous: api.generate.analogous(rgb),
    tetradic: api.generate.tetradic(rgb),
  };
 
  return (
    <div className="space-y-4">
      {Object.entries(harmonies).map(([name, colors]) => (
        <div key={name}>
          <h4 className="text-sm font-semibold capitalize mb-2">{name}</h4>
          <div className="flex gap-2">
            {colors.map((color, i) => {
              const hex = api.transforms.rgbToHex(color);
              return (
                <div
                  key={i}
                  className="flex-1 h-16 rounded cursor-pointer hover:scale-105 transition"
                  style={{ backgroundColor: hex.hex }}
                  title={hex.hex}
                  onClick={() => navigator.clipboard.writeText(hex.hex)}
                />
              );
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

Shades and Tints

function ShadesAndTints({ baseColor, api }: { baseColor: string; api: ColorAssemblyAPI }) {
  const rgb = api.transforms.hexToRgb({ hex: baseColor });
  const shades = api.generate.shades(rgb, 5);
  const tints = api.generate.tints(rgb, 5);
 
  return (
    <div className="space-y-4">
      {/* Shades */}
      <div>
        <h4 className="text-sm font-semibold mb-2">Shades (Darker)</h4>
        <div className="grid grid-cols-5 gap-2">
          {shades.map((color, i) => {
            const hex = api.transforms.rgbToHex(color);
            return (
              <div key={i} className="space-y-1">
                <div
                  className="w-full h-12 rounded cursor-pointer"
                  style={{ backgroundColor: hex.hex }}
                  onClick={() => navigator.clipboard.writeText(hex.hex)}
                />
                <div className="text-xs text-center font-mono">{hex.hex}</div>
              </div>
            );
          })}
        </div>
      </div>
 
      {/* Tints */}
      <div>
        <h4 className="text-sm font-semibold mb-2">Tints (Lighter)</h4>
        <div className="grid grid-cols-5 gap-2">
          {tints.map((color, i) => {
            const hex = api.transforms.rgbToHex(color);
            return (
              <div key={i} className="space-y-1">
                <div
                  className="w-full h-12 rounded cursor-pointer"
                  style={{ backgroundColor: hex.hex }}
                  onClick={() => navigator.clipboard.writeText(hex.hex)}
                />
                <div className="text-xs text-center font-mono">{hex.hex}</div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Accessibility Preview

function AccessibilityPreview({ baseColor, api }: { baseColor: string; api: ColorAssemblyAPI }) {
  const rgb = api.transforms.hexToRgb({ hex: baseColor });
  const textColor = api.analyze.suggestTextColor(rgb);
  const textHex = api.transforms.rgbToHex(textColor);
  const contrast = api.analyze.contrastRatio(textColor, rgb);
  const wcag = api.analyze.checkContrast(textColor, rgb);
 
  return (
    <div className="space-y-4">
      {/* Preview */}
      <div
        className="p-6 rounded-lg"
        style={{ backgroundColor: baseColor, color: textHex.hex }}
      >
        <h3 className="text-2xl font-bold mb-2">Sample Heading</h3>
        <p>This is how text will appear on this background color.</p>
      </div>
 
      {/* Contrast Info */}
      <div className="p-4 bg-gray-50 rounded">
        <div className="flex items-center justify-between mb-2">
          <span className="text-sm font-semibold">Contrast Ratio</span>
          <span className="text-2xl font-bold">{contrast.toFixed(2)}:1</span>
        </div>
        
        <div className="space-y-1 text-sm">
          <div className={wcag.normalTextAA ? 'text-green-600' : 'text-red-600'}>
            {wcag.normalTextAA ? '✓' : '✗'} Normal Text AA (4.5:1)
          </div>
          <div className={wcag.normalTextAAA ? 'text-green-600' : 'text-red-600'}>
            {wcag.normalTextAAA ? '✓' : '✗'} Normal Text AAA (7:1)
          </div>
          <div className={wcag.largeTextAA ? 'text-green-600' : 'text-red-600'}>
            {wcag.largeTextAA ? '✓' : '✗'} Large Text AA (3:1)
          </div>
        </div>
      </div>
    </div>
  );
}

Complete Color Picker Component

Putting it all together:

'use client';
 
import { useState, useEffect } from 'react';
import { loadColorAssembly, type ColorAssemblyAPI } from '@cymbol/colorassembly';
 
export function AdvancedColorPicker() {
  const [api, setApi] = useState<ColorAssemblyAPI | null>(null);
  const [color, setColor] = useState('#6366f1');
  const [activeTab, setActiveTab] = useState<'formats' | 'harmonies' | 'shades' | 'accessibility'>('formats');
 
  useEffect(() => {
    loadColorAssembly().then(setApi);
  }, []);
 
  if (!api) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="text-center">
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
          <p className="text-sm text-gray-600">Loading ColorAssembly...</p>
        </div>
      </div>
    );
  }
 
  const tabs = [
    { id: 'formats', label: 'Formats' },
    { id: 'harmonies', label: 'Harmonies' },
    { id: 'shades', label: 'Shades & Tints' },
    { id: 'accessibility', label: 'Accessibility' },
  ];
 
  return (
    <div className="w-full max-w-2xl mx-auto space-y-6 p-6 border rounded-lg shadow-lg">
      {/* Header */}
      <div>
        <h2 className="text-2xl font-bold mb-2">Color Picker</h2>
        <p className="text-sm text-gray-600">
          Pick a color and explore its properties, harmonies, and accessibility
        </p>
      </div>
 
      {/* Color Preview */}
      <div
        className="w-full h-32 rounded-lg shadow-md transition-colors duration-300"
        style={{ backgroundColor: color }}
      />
 
      {/* Color Input */}
      <input
        type="color"
        value={color}
        onChange={(e) => setColor(e.target.value)}
        className="w-full h-12 rounded-lg cursor-pointer"
      />
 
      {/* Tabs */}
      <div className="flex border-b">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id as any)}
            className={`px-4 py-2 font-medium transition-colors ${
              activeTab === tab.id
                ? 'border-b-2 border-blue-600 text-blue-600'
                : 'text-gray-600 hover:text-gray-900'
            }`}
          >
            {tab.label}
          </button>
        ))}
      </div>
 
      {/* Tab Content */}
      <div className="py-4">
        {activeTab === 'formats' && <ColorFormats color={color} api={api} />}
        {activeTab === 'harmonies' && <ColorHarmonies baseColor={color} api={api} />}
        {activeTab === 'shades' && <ShadesAndTints baseColor={color} api={api} />}
        {activeTab === 'accessibility' && <AccessibilityPreview baseColor={color} api={api} />}
      </div>
    </div>
  );
}

Tips

Debounce Color Changes

For better performance with live updates:

import { useState, useEffect } from 'react';
 
function useDebouncedColor(color: string, delay: number = 300) {
  const [debouncedColor, setDebouncedColor] = useState(color);
 
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedColor(color), delay);
    return () => clearTimeout(timer);
  }, [color, delay]);
 
  return debouncedColor;
}

Save Recent Colors

function useRecentColors() {
  const [recent, setRecent] = useState<string[]>([]);
 
  const addColor = (color: string) => {
    setRecent((prev) => {
      const filtered = prev.filter((c) => c !== color);
      return [color, ...filtered].slice(0, 10);
    });
  };
 
  return { recent, addColor };
}

Next Steps

On this page