Skip to content

ColorWheel

A circular 2D area component for adjusting two color channels mapped to angle and radius.

Preview

Source code
tsx
import "internationalized-color/css";
import { ColorWheel, useColor } from "@urcolor/react";

export default function ColorWheelHS() {
  const { color, setColor, hex } = useColor("hsl(210, 80%, 50%)");

  return (
    <>
      <code>{hex}</code>
      <ColorWheel.Root
        value={color}
        onValueChange={setColor}
        colorSpace="hsl"
        channelAngle="h"
        channelRadius="s"
        className="relative block size-64 overflow-hidden rounded-full"
        style={{ containerType: "inline-size" }}
      >
        <ColorWheel.Gradient className="absolute inset-0 block" />
        <ColorWheel.Thumb
          className="
            size-4 rounded-full border-2 border-white
            shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
            focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
          "
        />
      </ColorWheel.Root>
    </>
  );
}

Anatomy

tsx
<ColorWheel.Root>
  <ColorWheel.Checkerboard />
  <ColorWheel.Gradient />
  <ColorWheel.Thumb />
</ColorWheel.Root>

Examples

HSL / Hue x Saturation

HSL color wheel with Hue mapped to angle and Saturation to radius.

Source code
tsx
import "internationalized-color/css";
import { ColorWheel, useColor } from "@urcolor/react";

export default function ColorWheelHS() {
  const { color, setColor, hex } = useColor("hsl(210, 80%, 50%)");

  return (
    <>
      <code>{hex}</code>
      <ColorWheel.Root
        value={color}
        onValueChange={setColor}
        colorSpace="hsl"
        channelAngle="h"
        channelRadius="s"
        className="relative block size-64 overflow-hidden rounded-full"
        style={{ containerType: "inline-size" }}
      >
        <ColorWheel.Gradient className="absolute inset-0 block" />
        <ColorWheel.Thumb
          className="
            size-4 rounded-full border-2 border-white
            shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
            focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
          "
        />
      </ColorWheel.Root>
    </>
  );
}

HSL / Hue x Lightness

HSL color wheel with Hue mapped to angle and Lightness to radius.

Source code
tsx
import "internationalized-color/css";
import { ColorWheel, useColor } from "@urcolor/react";

export default function ColorWheelHL() {
  const { color, setColor, hex } = useColor("hsl(210, 80%, 50%)");

  return (
    <>
      <code>{hex}</code>
      <ColorWheel.Root
        value={color}
        onValueChange={setColor}
        colorSpace="hsl"
        channelAngle="h"
        channelRadius="l"
        className="relative block size-64 overflow-hidden rounded-full"
        style={{ containerType: "inline-size" }}
      >
        <ColorWheel.Gradient className="absolute inset-0 block" />
        <ColorWheel.Thumb
          className="
            size-4 rounded-full border-2 border-white
            shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
            focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
          "
        />
      </ColorWheel.Root>
    </>
  );
}

OKLCh / Hue x Chroma

OKLCh color wheel with Hue mapped to angle and Chroma to radius.

Source code
tsx
import "internationalized-color/css";
import { ColorWheel, useColor } from "@urcolor/react";

export default function ColorWheelOKLCh() {
  const { color, setColor, hex } = useColor("oklch(0.6 0.15 210)");

  return (
    <>
      <code>{hex}</code>
      <ColorWheel.Root
        value={color}
        onValueChange={setColor}
        colorSpace="oklch"
        channelAngle="h"
        channelRadius="c"
        className="relative block size-64 overflow-hidden rounded-full"
        style={{ containerType: "inline-size" }}
      >
        <ColorWheel.Gradient className="absolute inset-0 block" />
        <ColorWheel.Thumb
          className="
            size-4 rounded-full border-2 border-white
            shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_2px_4px_rgba(0,0,0,0.3)]
            focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
          "
        />
      </ColorWheel.Root>
    </>
  );
}

API Reference

ColorWheel.Root

The root container that manages wheel state and color channel binding.

PropTypeDefaultDescription
valueColor | string | nullControlled color value.
defaultValueColor | string'hsl(0, 100%, 50%)'Initial color when uncontrolled.
colorSpacestring'hsl'Color space mode (e.g. 'hsl', 'oklch').
channelAnglestringAutoChannel mapped to the angle axis. Auto-derived from color space.
channelRadiusstringAutoChannel mapped to the radius axis. Auto-derived from color space.
startAnglenumber0Starting angle offset in degrees.
disabledbooleanfalseDisables interaction.
onValueChange(color: Color) => voidCalled when color changes.
onValueCommit(color: Color) => voidCalled when interaction ends.

ColorWheel.Gradient

Renders a polar gradient canvas for the wheel.

PropTypeDefaultDescription
channelOverridesRecord<string, number> | false{ alpha: 1 }Lock specific channels to fixed values in the gradient.

ColorWheel.Checkerboard

Renders a checkerboard pattern behind the gradient to visualize alpha transparency.

ColorWheel.Thumb

Wrapper for the thumb indicator. Position is set automatically using polar coordinates.

ColorWheel.ThumbX / ColorWheel.ThumbY

Individual axis thumb elements for angle and radius keyboard navigation.

Accessibility

ColorWheel provides a circular 2D interface with two independently focusable thumb elements for keyboard access to the angle and radius axes.

Keyboard Navigation

KeyAction
Arrow RightIncrease angle by one step
Arrow LeftDecrease angle by one step
Arrow UpIncrease radius by one step
Arrow DownDecrease radius by one step
Shift + ArrowMove by 10 steps
Page Up / Page DownIncrease/decrease radius by 10 steps
HomeMove to minimum
EndMove to maximum