Color Picker

A color selection component that allows users to pick a color using a saturation/brightness area, channel sliders, and preset swatches.

import { ColorPicker } from "~/components/color-picker";

export default function ColorPickerBasicDemo() {
  return <ColorPicker label="Color" presets={["#ff0000", "#00ff00", "#0000ff"]} />;
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add color-picker

Manual

Create the recipe file at src/components/recipes/color-picker.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const colorPickerVariants = tv({
  slots: {
    root: "flex flex-col gap-1.5",
    control: "flex items-center gap-2",
    trigger: [
      "inline-flex items-center justify-center rounded-md border border-input transition-colors",
      "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
      "disabled:cursor-not-allowed disabled:opacity-50",
    ],
    content: [
      "z-50 w-64 flex flex-col gap-3 rounded-lg border bg-popover p-4 shadow-md outline-none",
      "data-[state=open]:animate-in data-[state=closed]:animate-out",
    ],
    area: "relative h-40 w-full overflow-hidden rounded-md",
    areaThumb:
      "absolute -translate-x-1/2 -translate-y-1/2 h-4 w-4 rounded-full border-2 border-white shadow-md",
    channelSlider: "relative h-3 w-full rounded-full",
    channelSliderTrack: "h-full w-full rounded-full",
    channelSliderThumb:
      "absolute -translate-x-1/2 -translate-y-1/2 h-3 w-3 rounded-full border-2 border-white shadow-md top-1/2",
    channelSliderLabel: "text-xs text-muted-foreground",
    channelInput: [
      "h-8 rounded-md border border-input bg-background px-2.5 py-1.5 text-base md:text-sm text-center ring-offset-background transition-colors",
      "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
      "disabled:cursor-not-allowed disabled:opacity-50",
    ],
    valueSwatch: "h-6 w-6 overflow-hidden rounded-md border",
    eyeDropperTrigger: [
      "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
      "hover:bg-accent hover:text-accent-foreground h-7 w-7",
    ],
    formatSelect:
      "flex h-7 rounded-md border border-input bg-transparent px-1 py-1 text-xs shadow-sm",
    formatTrigger: [
      "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
      "hover:bg-accent hover:text-accent-foreground h-7 w-7",
    ],
    swatchGroup: "flex flex-wrap gap-1",
    swatch: "h-6 w-6 overflow-hidden rounded-md border cursor-pointer",
    areaBackground: "h-full w-full rounded-[inherit]",
    transparencyGrid: "h-full w-full rounded-[inherit]",
    view: "flex flex-col gap-3",
    valueText: "text-xs text-muted-foreground",
    swatchIndicator: "absolute inset-0 flex items-center justify-center",
  },
  variants: {
    size: {
      sm: { trigger: "h-8 w-8" },
      md: { trigger: "h-8 w-8" },
      lg: { trigger: "h-8 w-8" },
    },
    inline: {
      true: { root: "relative" },
    },
  },
  defaultVariants: {
    size: "md",
  },
});

export type ColorPickerVariants = VariantProps<typeof colorPickerVariants>;

Create the component directory and files.

src/components/color-picker/color-picker.base.tsx:

import { ColorPicker as ArkColorPicker } from "@ark-ui/solid/color-picker";
import { splitProps, type Component } from "solid-js";
import { colorPickerVariants, labelVariants } from "../recipes/color-picker";

const styles = colorPickerVariants();

// ── Root (styled, with size + inline variants) ──────────────

const Root: Component<ArkColorPicker.RootProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Root class={styles.root({ class: local.class })} {...others} />;
};
const RootProvider = ArkColorPicker.RootProvider;

// ── Label ────────────────────────────────────────────────────

const Label: Component<ArkColorPicker.LabelProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Label class={labelVariants({ class: local.class })} {...others} />;
};

// ── Control ──────────────────────────────────────────────────

const Control: Component<ArkColorPicker.ControlProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Control class={styles.control({ class: local.class })} {...others} />;
};

// ── Trigger ──────────────────────────────────────────────────

const Trigger: Component<ArkColorPicker.TriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Trigger class={styles.trigger({ class: local.class })} {...others} />;
};

// ── Hidden Input (pass-through) ─────────────────────────────

const HiddenInput = ArkColorPicker.HiddenInput;

// ── Positioner (pass-through) ───────────────────────────────

const Positioner = ArkColorPicker.Positioner;

// ── Content ─────────────────────────────────────────────────

const Content: Component<ArkColorPicker.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Content class={styles.content({ class: local.class })} {...others} />;
};

// ── Area ─────────────────────────────────────────────────────

const Area: Component<ArkColorPicker.AreaProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Area class={styles.area({ class: local.class })} {...others} />;
};

// ── Area Background ───────────────────────────────────────────

const AreaBackground: Component<ArkColorPicker.AreaBackgroundProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.AreaBackground
      class={styles.areaBackground({ class: local.class })}
      {...others}
    />
  );
};

// ── Area Thumb ───────────────────────────────────────────────

const AreaThumb: Component<ArkColorPicker.AreaThumbProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.AreaThumb class={styles.areaThumb({ class: local.class })} {...others} />;
};

// ── Channel Slider ───────────────────────────────────────────

const ChannelSlider: Component<ArkColorPicker.ChannelSliderProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ChannelSlider
      class={styles.channelSlider({ class: local.class })}
      {...others}
    />
  );
};

// ── Channel Slider Track ─────────────────────────────────────

const ChannelSliderTrack: Component<ArkColorPicker.ChannelSliderTrackProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ChannelSliderTrack
      class={styles.channelSliderTrack({ class: local.class })}
      {...others}
    />
  );
};

// ── Channel Slider Thumb ─────────────────────────────────────

const ChannelSliderThumb: Component<ArkColorPicker.ChannelSliderThumbProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ChannelSliderThumb
      class={styles.channelSliderThumb({ class: local.class })}
      {...others}
    />
  );
};

// ── Channel Slider Label ─────────────────────────────────────

const ChannelSliderLabel: Component<ArkColorPicker.ChannelSliderLabelProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ChannelSliderLabel
      class={styles.channelSliderLabel({ class: local.class })}
      {...others}
    />
  );
};

// ── Channel Slider Value Text (pass-through) ────────────────

const ChannelSliderValueText = ArkColorPicker.ChannelSliderValueText;

// ── Channel Input ────────────────────────────────────────────

const ChannelInput: Component<ArkColorPicker.ChannelInputProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ChannelInput class={styles.channelInput({ class: local.class })} {...others} />
  );
};

// ── Value Swatch ─────────────────────────────────────────────

const ValueSwatch: Component<ArkColorPicker.ValueSwatchProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.ValueSwatch class={styles.valueSwatch({ class: local.class })} {...others} />
  );
};

// ── Value Text (pass-through) ───────────────────────────────

const ValueText = ArkColorPicker.ValueText;

// ── Eye Dropper Trigger ─────────────────────────────────────

const EyeDropperTrigger: Component<ArkColorPicker.EyeDropperTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.EyeDropperTrigger
      class={styles.eyeDropperTrigger({ class: local.class })}
      {...others}
    />
  );
};

// ── Format Select ────────────────────────────────────────────

const FormatSelect: Component<ArkColorPicker.FormatSelectProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.FormatSelect class={styles.formatSelect({ class: local.class })} {...others} />
  );
};

// ── Format Trigger ───────────────────────────────────────────

const FormatTrigger: Component<ArkColorPicker.FormatTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.FormatTrigger
      class={styles.formatTrigger({ class: local.class })}
      {...others}
    />
  );
};

// ── Swatch Group ─────────────────────────────────────────────

const SwatchGroup: Component<ArkColorPicker.SwatchGroupProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.SwatchGroup class={styles.swatchGroup({ class: local.class })} {...others} />
  );
};

// ── Swatch ───────────────────────────────────────────────────

const Swatch: Component<ArkColorPicker.SwatchProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.Swatch class={styles.swatch({ class: local.class })} {...others} />;
};

// ── Swatch Indicator ─────────────────────────────────────────

const SwatchIndicator: Component<ArkColorPicker.SwatchIndicatorProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.SwatchIndicator
      class={styles.swatchIndicator({ class: local.class })}
      {...others}
    />
  );
};

// ── Swatch Trigger ───────────────────────────────────────────

const SwatchTrigger: Component<ArkColorPicker.SwatchTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.SwatchTrigger class={styles.swatch({ class: local.class })} {...others} />;
};

// ── Transparency Grid ────────────────────────────────────────

const TransparencyGrid: Component<ArkColorPicker.TransparencyGridProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkColorPicker.TransparencyGrid
      class={styles.transparencyGrid({ class: local.class })}
      {...others}
    />
  );
};

// ── View ─────────────────────────────────────────────────────

const View: Component<ArkColorPicker.ViewProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkColorPicker.View class={styles.view({ class: local.class })} {...others} />;
};

// ── Context (pass-through) ──────────────────────────────────

const Context = ArkColorPicker.Context;

// ── Namespace Export ────────────────────────────────────────

export const ColorPickerBase = {
  Root,
  RootProvider,
  Label,
  Control,
  Trigger,
  HiddenInput,
  Positioner,
  Content,
  Area,
  AreaBackground,
  AreaThumb,
  ChannelSlider,
  ChannelSliderTrack,
  ChannelSliderThumb,
  ChannelSliderLabel,
  ChannelSliderValueText,
  ChannelInput,
  ValueSwatch,
  ValueText,
  EyeDropperTrigger,
  FormatSelect,
  FormatTrigger,
  SwatchGroup,
  Swatch,
  SwatchIndicator,
  SwatchTrigger,
  TransparencyGrid,
  View,
  Context,
};

src/components/color-picker/index.tsx:

import { For, Show, splitProps, type Component, type JSX } from "solid-js";
import { ColorPicker as ArkColorPicker, parseColor } from "@ark-ui/solid/color-picker";
import { Portal } from "solid-js/web";
import { ColorPickerBase } from "./color-picker.base";

const CheckIcon: Component<JSX.SvgSVGAttributes<SVGSVGElement>> = (props) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class="size-4"
    {...props}
  >
    <path d="M20 6L9 17l-5-5" />
    <title>Check</title>
  </svg>
);

// ── Composite: ColorPicker ───────────────────────────────────

type ColorPickerProps = ArkColorPicker.RootProps & {
  label?: string;
  presets?: string[];
  inline?: boolean;
};

const PickerContent: Component<{ presets?: string[]; children?: JSX.Element }> = (props) => {
  const presetColors = () => props.presets?.map((c) => parseColor(c));
  return (
    <ColorPickerBase.Content>
      <ColorPickerBase.Area>
        <ColorPickerBase.AreaBackground />
        <ColorPickerBase.AreaThumb />
      </ColorPickerBase.Area>
      <ColorPickerBase.ChannelSlider channel="hue">
        <ColorPickerBase.ChannelSliderTrack />
        <ColorPickerBase.ChannelSliderThumb />
      </ColorPickerBase.ChannelSlider>
      <ColorPickerBase.FormatSelect />
      <ColorPickerBase.ChannelInput channel="hex" />
      <Show when={presetColors()}>
        <ColorPickerBase.SwatchGroup>
          <For each={presetColors()}>
            {(color) => (
              <ColorPickerBase.SwatchTrigger value={color}>
                <ColorPickerBase.Swatch value={color}>
                  <ColorPickerBase.SwatchIndicator>
                    <CheckIcon />
                  </ColorPickerBase.SwatchIndicator>
                </ColorPickerBase.Swatch>
              </ColorPickerBase.SwatchTrigger>
            )}
          </For>
        </ColorPickerBase.SwatchGroup>
      </Show>
      {props.children}
    </ColorPickerBase.Content>
  );
};

const ColorPicker: Component<ColorPickerProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "label", "presets", "inline", "children"]);

  return (
    <ColorPickerBase.Root class={local.class} {...others}>
      <Show when={local.label}>
        <ColorPickerBase.Label>{local.label}</ColorPickerBase.Label>
      </Show>
      <ColorPickerBase.Control>
        <ColorPickerBase.ChannelInput channel="hex" />
        <ColorPickerBase.Trigger>
          <ColorPickerBase.TransparencyGrid />
          <ColorPickerBase.ValueSwatch />
        </ColorPickerBase.Trigger>
      </ColorPickerBase.Control>
      <Show
        when={local.inline}
        fallback={
          <Portal>
            <ColorPickerBase.Positioner>
              <PickerContent presets={local.presets}>{local.children}</PickerContent>
            </ColorPickerBase.Positioner>
          </Portal>
        }
      >
        <PickerContent presets={local.presets}>{local.children}</PickerContent>
      </Show>
      <ColorPickerBase.HiddenInput />
    </ColorPickerBase.Root>
  );
};

// ── Exports ──────────────────────────────────────────────────

export { ColorPicker, ColorPickerBase };

export { colorPickerVariants, type ColorPickerVariants } from "../recipes/color-picker";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Usage

Basic Usage

import { ColorPicker } from "~/components/color-picker";

export default function ColorPickerBasicDemo() {
  return <ColorPicker label="Color" presets={["#ff0000", "#00ff00", "#0000ff"]} />;
}

Inline

For inline rendering (no popover), use the inline prop:

import { ColorPicker } from "~/components/color-picker";
import { parseColor } from "@ark-ui/solid/color-picker";

export default function ColorPickerInlineDemo() {
  return (
    <div>
      <ColorPicker
        inline
        label="Inline Color Picker"
        defaultValue={parseColor("#eb5e41")}
        presets={["#ff0000", "#00ff00", "#0000ff"]}
      />
    </div>
  );
}

Controlled

Current color: rgba(235, 94, 65, 1)

import { createSignal } from "solid-js";
import { ColorPicker } from "~/components/color-picker";
import { parseColor } from "@ark-ui/solid/color-picker";

export default function ColorPickerControlledDemo() {
  const [color, setColor] = createSignal(parseColor("#eb5e41"));

  return (
    <div>
      <ColorPicker
        label="Color"
        value={color()}
        onValueChange={(e) => setColor(e.value)}
        presets={["#ff0000", "#00ff00", "#0000ff"]}
      />
      <p class="text-sm text-muted-foreground mt-3">
        Current color: <span class="font-mono text-foreground">{color().toString()}</span>
      </p>
    </div>
  );
}

Advanced Usage

For advanced layouts with custom composition, import ColorPickerBase to access individual parts:

import { Portal } from "solid-js/web";
import { parseColor } from "@ark-ui/solid/color-picker";
import { ColorPickerBase } from "~/components/color-picker";

export default function ColorPickerAdvancedDemo() {
  return (
    <ColorPickerBase.Root defaultValue={parseColor("#eb5e41")}>
      <ColorPickerBase.Label>Custom Color Picker</ColorPickerBase.Label>
      <ColorPickerBase.Control>
        {/* Custom trigger setup */}
        <ColorPickerBase.Trigger>
          <ColorPickerBase.TransparencyGrid />
          <ColorPickerBase.ValueSwatch />
        </ColorPickerBase.Trigger>
        <ColorPickerBase.ChannelInput channel="hex" />
      </ColorPickerBase.Control>

      <Portal>
        <ColorPickerBase.Positioner>
          <ColorPickerBase.Content>
            <ColorPickerBase.Area>
              <ColorPickerBase.AreaBackground />
              <ColorPickerBase.AreaThumb />
            </ColorPickerBase.Area>

            <div class="flex flex-col gap-3 mt-3">
              <ColorPickerBase.ChannelSlider channel="hue">
                <ColorPickerBase.ChannelSliderTrack />
                <ColorPickerBase.ChannelSliderThumb />
              </ColorPickerBase.ChannelSlider>

              <ColorPickerBase.ChannelSlider channel="alpha">
                <ColorPickerBase.TransparencyGrid />
                <ColorPickerBase.ChannelSliderTrack />
                <ColorPickerBase.ChannelSliderThumb />
              </ColorPickerBase.ChannelSlider>
            </div>
          </ColorPickerBase.Content>
        </ColorPickerBase.Positioner>
      </Portal>

      <ColorPickerBase.HiddenInput />
    </ColorPickerBase.Root>
  );
}

API Reference

See the Ark UI Color Picker documentation.