Aura Design System

Color Picker

A color field with a picker, a typed hex value, and swatches.

Preview

Loading...
import { ColorPicker } from "@/components/ui/ColorPicker";

export function ColorPickerDemo() {
  return <ColorPicker defaultValue="#964CE1" />;
}

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/color-picker

Manual

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the color swatch component into your components/ui/ColorSwatch.tsx file.

"use client";

import * as React from "react";
import { cn } from "@/utils/class-names";

function ColorSwatch({
  className,
  color,
  label,
  selected,
  ...props
}: React.ComponentProps<"button"> & {
  color: string;
  label: string;
  selected?: boolean;
}) {
  return (
    <button
      type="button"
      aria-label={label}
      aria-pressed={selected}
      data-slot="color-swatch"
      className={cn(
        "inline-flex size-3 items-center justify-center rounded-full border border-gray-7 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8 aria-pressed:ring-2 aria-pressed:ring-accent-9",
        className,
      )}
      style={{ backgroundColor: color }}
      {...props}
    />
  );
}

export { ColorSwatch };

Copy and paste the ColorPicker component into your components/ui/ColorPicker.tsx file.

"use client";

import * as React from "react";
import { cn } from "@/utils/class-names";
import { ColorSwatch } from "@/components/ui/ColorSwatch";

const presets = ["#964CE1", "#3D63DD", "#30A46C", "#E54D2E", "#F5D90A", "#111111"];

function ColorPicker({
  className,
  value,
  defaultValue = "#964CE1",
  onValueChange,
  label = "Color",
}: {
  className?: string;
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
  label?: string;
}) {
  const [uncontrolled, setUncontrolled] = React.useState(defaultValue);
  const [text, setText] = React.useState(value ?? defaultValue);
  const current = value ?? uncontrolled;
  const id = React.useId();

  const commit = (next: string) => {
    if (value === undefined) setUncontrolled(next);
    setText(next);
    onValueChange?.(next);
  };

  return (
    <div data-slot="color-picker" className={cn("flex w-fit flex-col gap-0.5", className)}>
      <label htmlFor={id}>{label}</label>
      <div className="flex items-center gap-0.5">
        <input
          type="color"
          aria-label={`${label} picker`}
          value={current}
          className="size-4 shrink-0 cursor-pointer bg-transparent"
          onChange={(event) => commit(event.target.value)}
        />
        <input
          id={id}
          value={text}
          spellCheck={false}
          className="w-12 rounded-sm border border-gray-7 bg-gray-1 px-1 py-0.5 text-gray-12 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8"
          onChange={(event) => {
            const next = event.target.value;
            setText(next);
            if (/^#[0-9a-fA-F]{6}$/.test(next)) commit(next);
          }}
        />
      </div>
      <div className="flex items-center gap-0.5" role="group" aria-label="Preset colors">
        {presets.map((preset) => (
          <ColorSwatch
            key={preset}
            color={preset}
            label={preset}
            selected={current.toLowerCase() === preset.toLowerCase()}
            onClick={() => commit(preset)}
          />
        ))}
      </div>
    </div>
  );
}

export { ColorPicker };