Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Color Picker

A comprehensive color picker component for selecting colors with multiple input methods and presets.

<ColorPickerWithPresets
  defaultValue="#3b82f6"
  onChange={(color) => console.log("Selected color:", color)}
/>

Installation

Install following dependencies:

npm install react-aria-components class-variance-authority lucide-react
pnpm add react-aria-components class-variance-authority lucide-react
yarn add react-aria-components class-variance-authority lucide-react
bun add react-aria-components class-variance-authority lucide-react

Copy and paste the following code into your project.

components/ui/ColorPicker/color-picker.tsx
"use client";

import * as React from "react";
import {
  ColorArea as AriaColorArea,
  ColorAreaProps as AriaColorAreaProps,
  ColorField as AriaColorField,
  ColorFieldProps as AriaColorFieldProps,
  ColorPicker as AriaColorPicker,
  ColorPickerProps as AriaColorPickerProps,
  ColorSlider as AriaColorSlider,
  ColorSliderProps as AriaColorSliderProps,
  ColorSwatch as AriaColorSwatch,
  ColorSwatchPicker as AriaColorSwatchPicker,
  ColorSwatchPickerItem as AriaColorSwatchPickerItem,
  ColorSwatchPickerItemProps as AriaColorSwatchPickerItemProps,
  ColorSwatchPickerProps as AriaColorSwatchPickerProps,
  ColorSwatchProps as AriaColorSwatchProps,
  ColorThumb as AriaColorThumb,
  ColorThumbProps as AriaColorThumbProps,
  SliderTrack as AriaSliderTrack,
  SliderTrackProps as AriaSliderTrackProps,
  ColorPickerStateContext,
  composeRenderProps,
  parseColor,
  FieldError,
  Label,
  Input,
  Group,
} from "react-aria-components";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Pipette } from "lucide-react";

const colorPickerVariants = cva(
  "flex flex-col gap-2 rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] p-4 ",
  {
    variants: {
      size: {
        sm: "max-w-xs",
        default: "max-w-sm",
        lg: "max-w-md",
      },
    },
    defaultVariants: {
      size: "default",
    },
  },
);

export interface ColorPickerProps
  extends AriaColorPickerProps,
    VariantProps<typeof colorPickerVariants> {
  className?: string;
}

function ColorPicker({
  className,
  size,
  children,
  ...props
}: ColorPickerProps) {
  return (
    <div className={cn(colorPickerVariants({ size }), className)}>
      <AriaColorPicker {...props}>{children}</AriaColorPicker>
    </div>
  );
}

function ColorField({ className, ...props }: AriaColorFieldProps) {
  return (
    <AriaColorField
      className={composeRenderProps(className, (className) =>
        cn("flex flex-col gap-2", className),
      )}
      {...props}
    />
  );
}

function ColorInput({
  className,
  ...props
}: React.ComponentProps<typeof Input>) {
  return (
    <Input
      className={composeRenderProps(className, (className) =>
        cn(
          "flex h-9 w-full rounded-md border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] px-3 py-1 text-sm text-[hsl(var(--hu-foreground))]  transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-[hsl(var(--hu-muted-foreground))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--hu-ring))] disabled:cursor-not-allowed disabled:opacity-50",
          className,
        ),
      )}
      {...props}
    />
  );
}

function ColorLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  return (
    <Label
      className={cn(
        "text-sm font-medium leading-none text-[hsl(var(--hu-foreground))] peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
        className,
      )}
      {...props}
    />
  );
}

function ColorArea({ className, ...props }: AriaColorAreaProps) {
  return (
    <AriaColorArea
      className={composeRenderProps(className, (className) =>
        cn(
          "h-[200px] w-full rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-gradient-to-br from-white to-black",
          className,
        ),
      )}
      {...props}
    />
  );
}

function ColorSlider({ className, ...props }: AriaColorSliderProps) {
  return (
    <AriaColorSlider
      className={composeRenderProps(className, (className) =>
        cn(
          "flex h-8 w-full flex-col gap-2 items-center justify-center",
          className,
        ),
      )}
      {...props}
    />
  );
}

function SliderTrack({ className, style, ...props }: AriaSliderTrackProps) {
  return (
    <AriaSliderTrack
      className={composeRenderProps(className, (className) =>
        cn(
          "relative h-3 w-full rounded-full border border-[hsl(var(--hu-border))]",
          className,
        ),
      )}
      style={({ defaultStyle }) => ({
        ...style,
        background: `${defaultStyle.background},
          repeating-conic-gradient(
            #ccc 0 90deg,
            #fff 0 180deg) 
          0% 0%/8px 8px`,
      })}
      {...props}
    />
  );
}

function ColorThumb({ className, ...props }: AriaColorThumbProps) {
  return (
    <AriaColorThumb
      className={composeRenderProps(className, (className) =>
        cn(
          "z-10 h-4 w-4 rounded-full border-2 border-white shadow-md ring-1 ring-black/10 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))] focus:ring-offset-2 data-[focus-visible]:ring-2 data-[focus-visible]:ring-[hsl(var(--hu-ring))]",
          className,
        ),
      )}
      {...props}
    />
  );
}

function ColorSwatchPicker({
  className,
  ...props
}: AriaColorSwatchPickerProps) {
  return (
    <AriaColorSwatchPicker
      className={composeRenderProps(className, (className) =>
        cn("flex flex-wrap gap-2", className),
      )}
      {...props}
    />
  );
}

function ColorSwatchPickerItem({
  className,
  ...props
}: AriaColorSwatchPickerItemProps) {
  return (
    <AriaColorSwatchPickerItem
      className={composeRenderProps(className, (className) =>
        cn(
          "group/swatch-item cursor-pointer rounded-[var(--radius)] p-1 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))] focus:ring-offset-2",
          className,
        ),
      )}
      {...props}
    />
  );
}

function ColorSwatch({ className, style, ...props }: AriaColorSwatchProps) {
  return (
    <AriaColorSwatch
      className={composeRenderProps(className, (className) =>
        cn(
          "h-8 w-8 rounded-md border border-[hsl(var(--hu-border))] group-data-[selected]/swatch-item:ring-2 group-data-[selected]/swatch-item:ring-[hsl(var(--hu-ring))] group-data-[selected]/swatch-item:ring-offset-2",
          className,
        ),
      )}
      style={({ defaultStyle }) => ({
        ...style,
        background: `${defaultStyle.background},
        repeating-conic-gradient(
          #ccc 0 90deg,
          #fff 0 180deg) 
        0% 0%/8px 8px`,
      })}
      {...props}
    />
  );
}

const EyeDropperButton = React.forwardRef<
  HTMLButtonElement,
  React.HTMLAttributes<HTMLButtonElement>
>(({ className, ...props }, ref) => {
  const state = React.useContext(ColorPickerStateContext);

  if (!state || typeof window === "undefined" || !("EyeDropper" in window)) {
    return null;
  }

  const handleEyeDropper = async () => {
    try {
      // @ts-ignore - EyeDropper API is not yet in TypeScript DOM types
      const eyeDropper = new window.EyeDropper();
      const result = await eyeDropper.open();
      state.setColor(parseColor(result.sRGBHex));
    } catch (error) {
      // User cancelled or error occurred
      console.warn("EyeDropper operation cancelled or failed:", error);
    }
  };

  return (
    <button
      ref={ref}
      type="button"
      className={cn(
        "inline-flex h-9 w-9 items-center justify-center rounded-md border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] text-[hsl(var(--hu-foreground))]  hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--hu-ring))] disabled:pointer-events-none disabled:opacity-50",
        className,
      )}
      onClick={handleEyeDropper}
      aria-label="Pick color from screen"
      {...props}
    >
      <Pipette className="h-4 w-4" />
    </button>
  );
});
EyeDropperButton.displayName = "EyeDropperButton";

function ColorError({
  className,
  ...props
}: React.ComponentProps<typeof FieldError>) {
  return (
    <FieldError
      className={composeRenderProps(className, (className) =>
        cn("text-sm font-medium text-[hsl(var(--hu-destructive))]", className),
      )}
      {...props}
    />
  );
}

export {
  ColorPicker,
  ColorField,
  ColorInput,
  ColorLabel,
  ColorArea,
  ColorSlider,
  SliderTrack,
  ColorThumb,
  ColorSwatchPicker,
  ColorSwatchPickerItem,
  ColorSwatch,
  EyeDropperButton,
  ColorError,
  Group as ColorGroup,
};
components/ui/ColorPicker/ColorPickerWithFormats.tsx
"use client";

import * as React from "react";
import {
  ColorPicker,
  ColorField,
  ColorInput,
  ColorLabel,
  ColorArea,
  ColorSlider,
  SliderTrack,
  ColorThumb,
  ColorSwatchPicker,
  ColorSwatchPickerItem,
  ColorSwatch,
  EyeDropperButton,
  ColorError,
  ColorGroup,
} from "./color-picker";
import { parseColor, Color } from "react-aria-components";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "../Select/select";
import {
  type ColorFormat,
  formatLabels,
  formatColorValue,
  parseColorFromFormat,
  getFormatPlaceholder,
} from "./color-utils";

interface ColorPickerWithFormatsProps {
  value?: string;
  defaultValue?: string;
  onChange?: (color: string) => void;
  presets?: string[];
  showEyeDropper?: boolean;
  showFormatSelector?: boolean;
  defaultFormat?: ColorFormat;
  className?: string;
  size?: "sm" | "default" | "lg";
}

const defaultPresets = [
  "#000000",
  "#ffffff",
  "#ff0000",
  "#00ff00",
  "#0000ff",
  "#ffff00",
  "#ff00ff",
  "#00ffff",
];

function ColorPickerWithFormats({
  value,
  defaultValue = "#000000",
  onChange,
  presets = defaultPresets,
  showEyeDropper = true,
  showFormatSelector = true,
  defaultFormat = "hex",
  className,
  size = "default",
}: ColorPickerWithFormatsProps) {
  const [colorValue, setColorValue] = React.useState(
    value ? parseColor(value) : parseColor(defaultValue),
  );
  const [currentFormat, setCurrentFormat] =
    React.useState<ColorFormat>(defaultFormat);
  const [inputValue, setInputValue] = React.useState("");

  React.useEffect(() => {
    if (value) {
      const parsed = parseColor(value);
      setColorValue(parsed);
      setInputValue(formatColorValue(parsed, currentFormat));
    }
  }, [value, currentFormat]);

  React.useEffect(() => {
    setInputValue(formatColorValue(colorValue, currentFormat));
  }, [colorValue, currentFormat]);

  const handleColorChange = React.useCallback(
    (color: Color) => {
      setColorValue(color);
      const formattedValue = formatColorValue(color, currentFormat);
      setInputValue(formattedValue);
      onChange?.(color.toString("hex"));
    },
    [onChange, currentFormat],
  );

  const handleInputChange = React.useCallback(
    (value: string) => {
      setInputValue(value);
      const parsed = parseColorFromFormat(value, currentFormat);
      if (parsed) {
        setColorValue(parsed);
        onChange?.(parsed.toString("hex"));
      }
    },
    [currentFormat, onChange],
  );

  const handleFormatChange = React.useCallback(
    (format: ColorFormat) => {
      setCurrentFormat(format);
      setInputValue(formatColorValue(colorValue, format));
    },
    [colorValue],
  );

  return (
    <ColorPicker
      value={colorValue}
      onChange={handleColorChange}
      size={size}
      className={className}
    >
      <div className="space-y-4">
        <ColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness">
          <ColorThumb />
        </ColorArea>

        <ColorSlider colorSpace="hsb" channel="hue">
          <SliderTrack>
            <ColorThumb />
          </SliderTrack>
        </ColorSlider>

        <ColorSlider colorSpace="hsb" channel="alpha">
          <SliderTrack>
            <ColorThumb />
          </SliderTrack>
        </ColorSlider>

        <div className="space-y-2">
          {showFormatSelector && (
            <div className="flex items-center gap-2">
              <ColorLabel className="min-w-0 shrink-0">Format</ColorLabel>
              <Select
                value={currentFormat}
                onValueChange={(value) =>
                  handleFormatChange(value as ColorFormat)
                }
              >
                <SelectTrigger className="w-24">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {Object.entries(formatLabels).map(([format, label]) => (
                    <SelectItem key={format} value={format}>
                      {label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
          )}{" "}
          <ColorField>
            <ColorLabel>{formatLabels[currentFormat]} Value</ColorLabel>
            <ColorGroup className="flex gap-2">
              <ColorInput
                value={inputValue}
                onChange={(e) => handleInputChange(e.target.value)}
                placeholder={getFormatPlaceholder(currentFormat)}
              />
              {showEyeDropper && <EyeDropperButton />}
            </ColorGroup>
            <ColorError />
          </ColorField>
        </div>

        {presets.length > 0 && (
          <div className="space-y-2">
            <ColorLabel>Presets</ColorLabel>
            <ColorSwatchPicker>
              {presets.map((preset, index) => (
                <ColorSwatchPickerItem key={index} color={parseColor(preset)}>
                  <ColorSwatch />
                </ColorSwatchPickerItem>
              ))}
            </ColorSwatchPicker>
          </div>
        )}
      </div>
    </ColorPicker>
  );
}

export {
  ColorPickerWithFormats,
  type ColorPickerWithFormatsProps,
  type ColorFormat,
};
components/ui/ColorPicker/ColorPickerWithPresets.tsx
"use client";

import * as React from "react";
import {
  ColorPicker,
  ColorField,
  ColorInput,
  ColorLabel,
  ColorArea,
  ColorSlider,
  SliderTrack,
  ColorThumb,
  ColorSwatchPicker,
  ColorSwatchPickerItem,
  ColorSwatch,
  EyeDropperButton,
  ColorError,
  ColorGroup,
} from "./color-picker";
import { parseColor } from "react-aria-components";

interface ColorPickerWithPresetsProps {
  value?: string;
  defaultValue?: string;
  onChange?: (color: string) => void;
  presets?: string[];
  showEyeDropper?: boolean;
  showInput?: boolean;
  className?: string;
  size?: "sm" | "default" | "lg";
}

const defaultPresets = [
  "#000000",
  "#ffffff",
  "#ff0000",
  "#00ff00",
  "#0000ff",
  "#ffff00",
  "#ff00ff",
];

function ColorPickerWithPresets({
  value,
  defaultValue = "#000000",
  onChange,
  presets = defaultPresets,
  showEyeDropper = true,
  showInput = true,
  className,
  size = "default",
}: ColorPickerWithPresetsProps) {
  const [colorValue, setColorValue] = React.useState(
    value ? parseColor(value) : parseColor(defaultValue),
  );

  React.useEffect(() => {
    if (value) {
      setColorValue(parseColor(value));
    }
  }, [value]);

  const handleColorChange = React.useCallback(
    (color: any) => {
      setColorValue(color);
      onChange?.(color.toString("hex"));
    },
    [onChange],
  );

  return (
    <ColorPicker
      value={colorValue}
      onChange={handleColorChange}
      size={size}
      className={className}
    >
      <div className="space-y-4">
        <ColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness">
          <ColorThumb />
        </ColorArea>

        <ColorSlider colorSpace="hsb" channel="hue">
          <SliderTrack>
            <ColorThumb />
          </SliderTrack>
        </ColorSlider>

        <ColorSlider colorSpace="hsb" channel="alpha">
          <SliderTrack>
            <ColorThumb />
          </SliderTrack>
        </ColorSlider>

        {showInput && (
          <ColorField>
            <ColorLabel>Hex Color</ColorLabel>
            <ColorGroup className="flex gap-2">
              <ColorInput />
              {showEyeDropper && <EyeDropperButton />}
            </ColorGroup>
            <ColorError />
          </ColorField>
        )}

        {presets.length > 0 && (
          <div className="space-y-2">
            <ColorLabel>Presets</ColorLabel>
            <ColorSwatchPicker>
              {presets.map((preset) => (
                <ColorSwatchPickerItem key={preset} color={parseColor(preset)}>
                  <ColorSwatch />
                </ColorSwatchPickerItem>
              ))}
            </ColorSwatchPicker>
          </div>
        )}
      </div>
    </ColorPicker>
  );
}

export { ColorPickerWithPresets };

Now create new file components/ui/ColorPicker/color-utils.ts and add the following code:

components/ui/ColorPicker/color-utils.ts
/**
 * Enhanced color format utilities for the ColorPicker component
 * Provides conversion between different color formats including OKLCH and LAB
 */

import { Color, parseColor } from "react-aria-components";

export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv" | "oklch" | "lab";

export const formatLabels: Record<ColorFormat, string> = {
  hex: "HEX",
  rgb: "RGB",
  hsl: "HSL",
  hsv: "HSV",
  oklch: "OKLCH",
  lab: "LAB",
};

/**
 * Converts RGB values (0-1) to XYZ color space
 */
function rgbToXyz(r: number, g: number, b: number): [number, number, number] {
  // Convert sRGB to linear RGB
  const toLinear = (c: number) => {
    return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  };

  const rLinear = toLinear(r);
  const gLinear = toLinear(g);
  const bLinear = toLinear(b);

  // Convert to XYZ using sRGB matrix
  const x = rLinear * 0.4124564 + gLinear * 0.3575761 + bLinear * 0.1804375;
  const y = rLinear * 0.2126729 + gLinear * 0.7151522 + bLinear * 0.072175;
  const z = rLinear * 0.0193339 + gLinear * 0.119192 + bLinear * 0.9503041;

  return [x, y, z];
}

/**
 * Converts XYZ to LAB color space
 */
function xyzToLab(x: number, y: number, z: number): [number, number, number] {
  // Reference white point D65
  const xn = 0.95047;
  const yn = 1.0;
  const zn = 1.08883;

  const fx = x / xn;
  const fy = y / yn;
  const fz = z / zn;

  const transform = (t: number) => {
    return t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116;
  };

  const fxT = transform(fx);
  const fyT = transform(fy);
  const fzT = transform(fz);

  const L = 116 * fyT - 16;
  const a = 500 * (fxT - fyT);
  const b = 200 * (fyT - fzT);

  return [L, a, b];
}

/**
 * Converts XYZ to OKLCH color space (simplified conversion)
 */
function xyzToOklch(x: number, y: number, z: number): [number, number, number] {
  // Simplified conversion to OKLCH
  // In practice, you'd want to use a proper color library like colorjs.io

  // Convert to OKLab first (simplified)
  const l = Math.cbrt(0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z);
  const m = Math.cbrt(0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z);
  const s = Math.cbrt(0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z);

  const okL = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
  const okA = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
  const okB = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;

  // Convert to LCH
  const L_oklch = okL;
  const C = Math.sqrt(okA * okA + okB * okB);
  const H = (Math.atan2(okB, okA) * 180) / Math.PI;

  return [L_oklch, C, H < 0 ? H + 360 : H];
}

/**
 * Formats a color value according to the specified format
 */
export function formatColorValue(color: Color, format: ColorFormat): string {
  switch (format) {
    case "hex":
      return color.toString("hex");
    case "rgb": {
      const rgb = color.toFormat("rgb");
      const r = Math.round(rgb.getChannelValue("red"));
      const g = Math.round(rgb.getChannelValue("green"));
      const b = Math.round(rgb.getChannelValue("blue"));
      const alpha = rgb.getChannelValue("alpha");

      if (alpha < 1) {
        return `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`;
      }
      return `rgb(${r}, ${g}, ${b})`;
    }
    case "hsl": {
      const hsl = color.toFormat("hsl");
      const h = Math.round(hsl.getChannelValue("hue"));
      const s = Math.round(hsl.getChannelValue("saturation"));
      const l = Math.round(hsl.getChannelValue("lightness"));
      const alpha = hsl.getChannelValue("alpha");

      if (alpha < 1) {
        return `hsla(${h}, ${s}%, ${l}%, ${alpha.toFixed(2)})`;
      }
      return `hsl(${h}, ${s}%, ${l}%)`;
    }
    case "hsv": {
      const hsv = color.toFormat("hsb"); // HSB is HSV in react-aria-components
      const h = Math.round(hsv.getChannelValue("hue"));
      const s = Math.round(hsv.getChannelValue("saturation"));
      const v = Math.round(hsv.getChannelValue("brightness"));
      const alpha = hsv.getChannelValue("alpha");

      if (alpha < 1) {
        return `hsva(${h}, ${s}%, ${v}%, ${alpha.toFixed(2)})`;
      }
      return `hsv(${h}, ${s}%, ${v}%)`;
    }
    case "oklch": {
      const rgb = color.toFormat("rgb");
      const r = rgb.getChannelValue("red") / 255;
      const g = rgb.getChannelValue("green") / 255;
      const b = rgb.getChannelValue("blue") / 255;
      const alpha = rgb.getChannelValue("alpha");

      const [x, y, z] = rgbToXyz(r, g, b);
      const [L, C, H] = xyzToOklch(x, y, z);

      if (alpha < 1) {
        return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(
          1,
        )} / ${alpha.toFixed(2)})`;
      }
      return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(1)})`;
    }
    case "lab": {
      const rgb = color.toFormat("rgb");
      const r = rgb.getChannelValue("red") / 255;
      const g = rgb.getChannelValue("green") / 255;
      const b = rgb.getChannelValue("blue") / 255;
      const alpha = rgb.getChannelValue("alpha");

      const [x, y, z] = rgbToXyz(r, g, b);
      const [L, a, b_lab] = xyzToLab(x, y, z);

      if (alpha < 1) {
        return `lab(${L.toFixed(1)}% ${a.toFixed(1)} ${b_lab.toFixed(
          1,
        )} / ${alpha.toFixed(2)})`;
      }
      return `lab(${L.toFixed(1)}% ${a.toFixed(1)} ${b_lab.toFixed(1)})`;
    }
    default:
      return color.toString("hex");
  }
}

/**
 * Parses a color string in the specified format
 */
export function parseColorFromFormat(
  value: string,
  format: ColorFormat,
): Color | null {
  try {
    // For formats that react-aria-components supports directly
    if (format === "hex" || format === "rgb" || format === "hsl") {
      return parseColor(value);
    }

    if (format === "hsv") {
      // Try to parse HSV/HSB format
      const hsvMatch = value.match(/hsva?\(([^)]+)\)/);
      if (hsvMatch) {
        const parts = hsvMatch[1].split(",").map((p) => p.trim());
        const h = parseFloat(parts[0]) || 0;
        const s = parseFloat(parts[1]) || 0;
        const v = parseFloat(parts[2]) || 0;
        const a = parts[3] ? parseFloat(parts[3]) : 1;

        // Convert HSV to HSL for react-aria-components
        const hslL = (v * (2 - s / 100)) / 2;
        const hslS = (v * s) / (100 - Math.abs(2 * hslL - 100));

        return parseColor(`hsla(${h}, ${hslS || 0}%, ${hslL}%, ${a})`);
      }
    }

    if (format === "oklch") {
      // Parse OKLCH and convert to HSL as approximation
      const oklchMatch = value.match(/oklch\(([^)]+)\)/);
      if (oklchMatch) {
        const parts = oklchMatch[1].split(/[\s\/]+/);
        const L = parseFloat(parts[0]) || 50;
        const C = parseFloat(parts[1]) || 0;
        const H = parseFloat(parts[2]) || 0;
        const alpha = parts[3] ? parseFloat(parts[3]) : 1;

        // Simplified conversion back to HSL
        return parseColor(
          `hsla(${H}, ${Math.min(C * 100, 100)}%, ${L}%, ${alpha})`,
        );
      }
    }

    if (format === "lab") {
      // Parse LAB and convert to HSL as approximation
      const labMatch = value.match(/lab\(([^)]+)\)/);
      if (labMatch) {
        const parts = labMatch[1].split(/[\s\/]+/);
        const L = parseFloat(parts[0]) || 50;
        const a = parseFloat(parts[1]) || 0;
        const b = parseFloat(parts[2]) || 0;
        const alpha = parts[3] ? parseFloat(parts[3]) : 1;

        // Simplified conversion back to HSL
        const chroma = Math.sqrt(a * a + b * b);
        const hue = (Math.atan2(b, a) * 180) / Math.PI;

        return parseColor(
          `hsla(${hue < 0 ? hue + 360 : hue}, ${Math.min(
            chroma,
            100,
          )}%, ${L}%, ${alpha})`,
        );
      }
    }

    // Fallback: try to parse as any supported format
    return parseColor(value);
  } catch {
    return null;
  }
}

/**
 * Validates if a color string is valid for the given format
 */
export function isValidColorFormat(
  value: string,
  format: ColorFormat,
): boolean {
  const parsed = parseColorFromFormat(value, format);
  return parsed !== null;
}

/**
 * Gets format-specific input placeholder text
 */
export function getFormatPlaceholder(format: ColorFormat): string {
  switch (format) {
    case "hex":
      return "#3b82f6";
    case "rgb":
      return "rgb(59, 130, 246)";
    case "hsl":
      return "hsl(220, 91%, 64%)";
    case "hsv":
      return "hsv(220, 76%, 96%)";
    case "oklch":
      return "oklch(65% 0.15 230)";
    case "lab":
      return "lab(55% -10 40)";
    default:
      return "";
  }
}
npx hextaui@latest add color-picker
pnpm dlx hextaui@latest add color-picker
yarn dlx hextaui@latest add color-picker
bun x hextaui@latest add color-picker

Usage

import { ColorPickerWithPresets } from "@/components/ui/ColorPicker";
<ColorPickerWithPresets
  defaultValue="#3b82f6"
  onChange={(color) => console.log("Selected color:", color)}
/>

Examples

With Custom Presets

<ColorPickerWithPresets
  defaultValue="#8b5cf6"
  presets={[
    "#ef4444",
    "#f97316",
    "#eab308",
    "#22c55e",
    "#06b6d4",
    "#3b82f6",
    "#8b5cf6",
    "#e11d48",
  ]}
/>

Controlled

Current color: #14b8a6

export function ControlledExample() {
const [color, setColor] = React.useState("#14b8a6");

return (
    <div className="space-y-4">
    <ColorPickerWithPresets value={color} onChange={setColor} />
    <p className="text-sm text-muted-foreground">Current color: {color}</p>
    </div>
);
}

Color Swatches Only

import { parseColor } from "react-aria-components";

<ColorPicker defaultValue={parseColor("#ef4444")}>
  <div className="space-y-3">
    <ColorLabel>Choose a color</ColorLabel>
    <ColorSwatchPicker>
      {colors.map((color) => (
        <ColorSwatchPickerItem key={color} color={parseColor(color)}>
          <ColorSwatch />
        </ColorSwatchPickerItem>
      ))}
    </ColorSwatchPicker>
  </div>
</ColorPicker>

Multiple Color Formats

The enhanced color picker supports multiple color formats including HEX, RGB, HSL, HSV, OKLCH, and LAB.

Color Picker with Format Selection

Current color: #3b82f6

import { ColorPickerWithFormats } from "@/components/ui/ColorPicker";

const [color, setColor] = React.useState("#3b82f6");

<ColorPickerWithFormats
  value={color}
  onChange={setColor}
  defaultFormat="hex"
  showFormatSelector={true}
  showEyeDropper={true}
/>

Props

ColorPickerWithPresets

PropTypeDefault
className?
string
undefined
size?
"sm" | "default" | "lg"
"default"
showInput?
boolean
true
showEyeDropper?
boolean
true
presets?
string[]
defaultPresets
onChange?
(color: string) => void
undefined
defaultValue?
string
"#000000"
value?
string
undefined

ColorPickerWithFormats

PropTypeDefault
className?
string
undefined
size?
"sm" | "default" | "lg"
"default"
defaultFormat?
"hex" | "rgb" | "hsl" | "hsv" | "oklch" | "lab"
"hex"
showFormatSelector?
boolean
true
showEyeDropper?
boolean
true
presets?
string[]
defaultPresets
onChange?
(color: string) => void
undefined
defaultValue?
string
"#000000"
value?
string
undefined
Edit on GitHub

Last updated on