Fiber UI LogoFiberUI

ColorArea

A color area allows users to adjust two channels of an RGB, HSL or HSB color value against a two-dimensional gradient background.

Basic Usage

#800080

hsb(300, 100%, 50%)

"use client";

import { useState } from "react";
import { parseColor, Color } from "react-aria-components";
import { ColorArea } from "@repo/ui/components/color-area";
import { Label } from "@repo/ui/components/label";
import {
    Select,
    SelectTrigger,
    SelectContent,
    SelectItem,
    SelectValue,
} from "@repo/ui/components/select";
import { Switch } from "@repo/ui/components/switch";

type ColorSpace = "rgb" | "hsl" | "hsb";
type Channel =
    | "red"
    | "green"
    | "blue"
    | "hue"
    | "saturation"
    | "lightness"
    | "brightness"
    | "alpha";

const CHANNEL_OPTIONS: Record<ColorSpace, Channel[]> = {
    rgb: ["red", "green", "blue", "alpha"],
    hsl: ["hue", "saturation", "lightness", "alpha"],
    hsb: ["hue", "saturation", "brightness", "alpha"],
};

const DEFAULT_COLORS: Record<ColorSpace, string> = {
    rgb: "rgb(128, 0, 128)",
    hsl: "hsl(300, 100%, 25%)",
    hsb: "hsb(300, 100%, 50%)",
};

/* INTERACTIVE COLOR AREA PLAYGROUND */
export const Example1: React.FC = () => {
    const [colorSpace, setColorSpace] = useState<ColorSpace>("hsb");
    const [xChannel, setXChannel] = useState<string>("default");
    const [yChannel, setYChannel] = useState<string>("default");
    const [isDisabled, setIsDisabled] = useState(false);
    const [color, setColor] = useState<Color>(parseColor(DEFAULT_COLORS.hsb));

    const handleColorSpaceChange = (newSpace: string) => {
        if (!newSpace || !DEFAULT_COLORS[newSpace as ColorSpace]) return;
        setColorSpace(newSpace as ColorSpace);
        setXChannel("default");
        setYChannel("default");
        setColor(parseColor(DEFAULT_COLORS[newSpace as ColorSpace]));
    };

    const channels = CHANNEL_OPTIONS[colorSpace];

    return (
        <div className="flex gap-8">
            {/* Left: Controls */}
            <div className="flex flex-col gap-4">
                <div className="space-y-1.5">
                    <Label>colorSpace</Label>
                    <Select
                        defaultValue={colorSpace}
                        value={colorSpace}
                        onChange={(key) =>
                            handleColorSpaceChange(key as string)
                        }
                    >
                        <SelectTrigger className="w-32">
                            <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectItem value="rgb">RGB</SelectItem>
                            <SelectItem value="hsl">HSL</SelectItem>
                            <SelectItem value="hsb">HSB</SelectItem>
                        </SelectContent>
                    </Select>
                </div>

                <div className="space-y-1.5">
                    <Label>xChannel</Label>
                    <Select
                        defaultValue="default"
                        value={xChannel}
                        onChange={(key) => setXChannel(key as string)}
                    >
                        <SelectTrigger className="w-32">
                            <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectItem value="default">Default</SelectItem>
                            {channels.map((ch) => (
                                <SelectItem key={ch} value={ch}>
                                    {ch}
                                </SelectItem>
                            ))}
                        </SelectContent>
                    </Select>
                </div>

                <div className="space-y-1.5">
                    <Label>yChannel</Label>
                    <Select
                        defaultValue="default"
                        value={yChannel}
                        onChange={(key) => setYChannel(key as string)}
                    >
                        <SelectTrigger className="w-32">
                            <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                            <SelectItem value="default">Default</SelectItem>
                            {channels.map((ch) => (
                                <SelectItem key={ch} value={ch}>
                                    {ch}
                                </SelectItem>
                            ))}
                        </SelectContent>
                    </Select>
                </div>

                <div className="flex items-center gap-2">
                    <Switch isSelected={isDisabled} onChange={setIsDisabled} />
                    <Label>isDisabled</Label>
                </div>
            </div>

            {/* Right: ColorArea + Output */}
            <div className="flex flex-col gap-4">
                <ColorArea
                    value={color}
                    onChange={setColor}
                    xChannel={
                        xChannel === "default"
                            ? undefined
                            : (xChannel as Channel)
                    }
                    yChannel={
                        yChannel === "default"
                            ? undefined
                            : (yChannel as Channel)
                    }
                    isDisabled={isDisabled}
                />

                <div className="flex items-center gap-3">
                    <div
                        className="size-10 rounded-md border shadow-sm"
                        style={{ backgroundColor: color.toString("css") }}
                    />
                    <div className="text-sm">
                        <p className="font-mono font-medium">
                            {color.toString("hex")}
                        </p>
                        <p className="text-muted-foreground font-mono">
                            {color.toString(colorSpace)}
                        </p>
                    </div>
                </div>
            </div>
        </div>
    );
};

Controlled

#00FF00

hsl(120, 100%, 50%)

"use client";

import { useState } from "react";
import { parseColor } from "react-aria-components";
import { ColorArea } from "@repo/ui/components/color-area";

/* CONTROLLED COLOR AREA WITH VALUE DISPLAY */
export const Example2: React.FC = () => {
    const [color, setColor] = useState(parseColor("hsl(120, 100%, 50%)"));

    return (
        <div className="flex flex-col gap-4">
            <ColorArea value={color} onChange={setColor} />
            <div className="flex items-center gap-3">
                <div
                    className="size-10 rounded-md border shadow-sm"
                    style={{ backgroundColor: color.toString("css") }}
                />
                <div className="text-sm">
                    <p className="font-medium">{color.toString("hex")}</p>
                    <p className="text-muted-foreground">
                        {color.toString("hsl")}
                    </p>
                </div>
            </div>
        </div>
    );
};

X/Y Channel Configuration

Use xChannel and yChannel props to configure which color channels are controlled by the horizontal and vertical axes.

Saturation vs Lightness

Hue vs Saturation

hsl(0, 100%, 50%)

"use client";

import { useState } from "react";
import { parseColor } from "react-aria-components";
import { ColorArea } from "@repo/ui/components/color-area";

/* COLOR AREA WITH DIFFERENT COLOR SPACES */
export const Example3: React.FC = () => {
    const [hslColor, setHslColor] = useState(parseColor("hsl(0, 100%, 50%)"));

    return (
        <div className="flex flex-col gap-4">
            <div className="grid grid-cols-2 gap-4">
                <div className="space-y-2">
                    <p className="text-sm font-medium">
                        Saturation vs Lightness
                    </p>
                    <ColorArea
                        value={hslColor}
                        onChange={setHslColor}
                        xChannel="saturation"
                        yChannel="lightness"
                    />
                </div>
                <div className="space-y-2">
                    <p className="text-sm font-medium">Hue vs Saturation</p>
                    <ColorArea
                        value={hslColor}
                        onChange={setHslColor}
                        xChannel="hue"
                        yChannel="saturation"
                    />
                </div>
            </div>
            <div className="flex items-center gap-3">
                <div
                    className="size-10 rounded-md border shadow-sm"
                    style={{ backgroundColor: hslColor.toString("css") }}
                />
                <p className="text-sm font-medium">
                    {hslColor.toString("hsl")}
                </p>
            </div>
        </div>
    );
};

Disabled

"use client";

import { parseColor } from "react-aria-components";
import { ColorArea } from "@repo/ui/components/color-area";

/* DISABLED COLOR AREA */
export const Example4: React.FC = () => {
    return <ColorArea defaultValue={parseColor("#7f007f")} isDisabled />;
};

Playground

Try out different color spaces (RGB, HSL, HSB) and channel configurations:

"use client";

import { parseColor } from "react-aria-components";
import { ColorArea } from "@repo/ui/components/color-area";

/* BASIC COLOR AREA EXAMPLE */
export const Example5: React.FC = () => {
    return <ColorArea defaultValue={parseColor("#7f007f")} />;
};

Component Code

"use client";
import {
    ColorArea as AriaColorArea,
    ColorAreaProps as AriaColorAreaProps,
} from "react-aria-components";
import { ColorThumb } from "@repo/ui/components/color-thumb";
import { cn } from "tailwind-variants";

export interface ColorAreaProps extends AriaColorAreaProps {}

export const ColorArea: React.FC<ColorAreaProps> = ({
    className,
    ...props
}) => {
    return (
        <AriaColorArea
            data-slot="color-area"
            {...props}
            className={cn(
                "data-disabled:bg-muted aspect-square w-56 shrink-0 rounded-lg",
                className,
            )}
            style={({ defaultStyle, isDisabled }) => ({
                ...defaultStyle,
                background: isDisabled ? undefined : defaultStyle.background,
            })}
        >
            <ColorThumb />
        </AriaColorArea>
    );
};