COMPONENTS/INPUTS

Color picker.

Choose a single accent.

In practice

Custom swatches and an editable hex field. Supports value/onChange or defaultValue for opaque sRGB colors.

Installation

Install Coal once. Components and TypeScript declarations are included. React 19 and React DOM 19 are peer dependencies.

Terminal · local package
npm install http://localhost:3100/downloads/chlohal-coal-ui-0.5.0.tgz

This installs the local package. It is not yet published on npm. See installation for stylesheet setup and integration.

Usage

example.tsx
"use client";
import * as React from "react";
import "@chlohal/coal-ui/styles.css";
import { ColorPicker, Label } from "@chlohal/coal-ui";
export default function Example() {
    const id = React.useId();
    return (<div>
          <Label htmlFor={id}>Accent color</Label>
          <ColorPicker id={id} defaultValue="#756887"/>
        </div>);
}

Source

The original Coal implementation. Use the package export to integrate it.

packages/react/src/color-picker.tsx
"use client";
import * as React from "react";
import { cn, setInputValue, useFormReset } from "./internal.js";
import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverTitle,
} from "./popover.js";
export type ColorPickerProps = Omit<
  React.ComponentPropsWithRef<"input">,
  "type" | "size"
>;
const palette = [
  "#242424",
  "#686868",
  "#a0a0a0",
  "#d9d9d9",
  "#f0f0f0",
  "#ffffff",
  "#756887",
  "#485c78",
  "#447a85",
  "#476d60",
  "#728ca0",
  "#a7bbc0",
];
export function ColorPicker({
  className,
  value,
  defaultValue = "#756887",
  onChange,
  ref,
  disabled,
  ...props
}: ColorPickerProps) {
  const [local, setLocal] = React.useState(String(defaultValue));
  const color = String(value ?? local);
  const input = React.useRef<HTMLInputElement>(null);
  useFormReset(input, () => setLocal(String(defaultValue)));
  return (
    <Popover>
      <div className={cn("coal-color-field", className)}>
        <input
          {...props}
          ref={(node) => {
            input.current = node;
            if (typeof ref === "function") ref(node);
            else if (ref) ref.current = node;
          }}
          type="text"
          spellCheck={false}
          pattern="#[a-fA-F0-9]{6}"
          maxLength={7}
          disabled={disabled}
          className="coal-input coal-color-picker"
          value={color}
          onChange={(e) => {
            onChange?.(e);
            if (!e.defaultPrevented) setLocal(e.target.value);
          }}
        />
        <PopoverTrigger
          disabled={disabled || props.readOnly}
          aria-label="Open color palette"
          className="coal-color-swatch"
          style={{
            backgroundColor: /^#[\da-f]{6}$/i.test(color) ? color : undefined,
          }}
        />
      </div>
      <PopoverContent className="coal-color-panel" aria-label="Color palette">
        <PopoverTitle>Choose a color</PopoverTitle>
        <p className="coal-picker-hint">
          Pick a swatch, or enter any six-digit hex value.
        </p>
        <div className="coal-color-grid">
          {palette.map((hex) => (
            <button
              key={hex}
              type="button"
              aria-label={`Use ${hex}`}
              aria-pressed={color.toLowerCase() === hex}
              style={{ backgroundColor: hex }}
              onClick={() => setInputValue(input.current, hex)}
            />
          ))}
        </div>
        <p className="coal-picker-hint">Current: {color}</p>
      </PopoverContent>
    </Popover>
  );
}

Accessibility checklist

  • Provide an accessible name for controls, including icon-only buttons.
  • Check keyboard navigation, visible focus and disabled states in your application.
  • Do not rely on color alone to communicate status.