# Color Area (/ui/components/react/color-area)




## Usage [#usage]

```tsx
import { ColorArea } from '@appica/ui-react/color-area'
import { parseColor } from '@appica/ui-react/color'
```

```tsx
<ColorArea defaultValue="hsb(217, 76%, 96%)" xChannel="saturation" yChannel="brightness" />
```

`ColorArea` paints a plane of a color space and puts a draggable thumb on it. The horizontal axis drives `xChannel`, the vertical axis drives `yChannel` (increasing upwards), and the third channel of the space is held constant - it's what the plane is a slice of. Pair it with a slider for that third channel and you have a color picker.

The value is a `Color`: a plain object you get from `parseColor`, or any CSS color string passed straight to `value` / `defaultValue`. Because it's a plain object and not a class instance, it serializes cleanly across the server/client boundary. Every handler is called with a `Color`, which you format with `formatColor`.

```tsx
const [color, setColor] = useState(() => parseColor('#3b82f6'))

<ColorArea value={color} onValueChange={setColor} xChannel="saturation" yChannel="brightness" />
```

The color space comes from the value, so `parseColor('hsl(…)')` gives an HSL area and a hex string gives an RGB one. Set `colorSpace` to override it and convert on the way in.

A **space** and a **format** are different things, and it's worth being clear about which is which. The space is the set of axes you move along (`rgb`, `hsl`, `hsb`); the format is how the result gets written down. Hex is a format, not a space - `#3b82f6` and `rgb(59, 130, 246)` are the same color in the same space, spelled differently. Pick in whichever space suits the control, then call `formatColor` for whatever you need to store.

Give the area a name with `aria-label`: it has no visible text, and the name is what a screen reader reads before the channel values.

Inside a [Color Picker](/ui/components/react/color-picker) the `value` and the handlers can be left off entirely: the area reads and writes the picker's color through context.

## Examples [#examples]

### Color spaces [#color-spaces]

Three spaces can be plotted: `rgb`, `hsl`, and `hsb`. Each is separable, so the browser paints the plane from two stacked CSS gradients over a solid base - no per-pixel work, and it renders during SSR. Below is the same blue, sliced along a different pair of channels in each.

```tsx
import { ColorArea } from '@appica/ui-react/color-area'

const PLANES = [
  { space: 'rgb', xChannel: 'red', yChannel: 'green', label: 'RGB' },
  { space: 'hsl', xChannel: 'saturation', yChannel: 'lightness', label: 'HSL' },
  { space: 'hsb', xChannel: 'saturation', yChannel: 'brightness', label: 'HSB' },
] as const

export default function ColorAreaSpaces() {
  return (
    <div className="grid grid-cols-3 gap-6">
      {PLANES.map(({ space, xChannel, yChannel, label }) => (
        <div key={space} className="flex flex-col items-center gap-2">
          <ColorArea
            defaultValue="#3b82f6"
            colorSpace={space}
            xChannel={xChannel}
            yChannel={yChannel}
            aria-label={`${label} color area`}
            className="size-32"
          />
          <span className="text-foreground-muted text-xs font-medium">{label}</span>
        </div>
      ))}
    </div>
  )
}
```

### Building a picker [#building-a-picker]

`ColorArea` covers two channels, so the third needs its own control: a [Color Slider](/ui/components/react/color-slider). Add a second one for alpha and a [Color Swatch](/ui/components/react/color-swatch) for the result, and you have the shape most pickers take. Every part reads and writes the same `Color`, so composing them is just sharing one piece of state.

```tsx
'use client'

import { useState } from 'react'
import { ColorArea } from '@appica/ui-react/color-area'
import { ColorSlider } from '@appica/ui-react/color-slider'
import { ColorSwatch } from '@appica/ui-react/color-swatch'
import { type Color, formatColor, parseColor } from '@appica/ui-react/color'

export default function ColorAreaPicker() {
  const [color, setColor] = useState<Color>(() => parseColor('hsb(217, 76%, 96%)'))

  return (
    <div className="flex w-full max-w-56 flex-col gap-3">
      <ColorArea
        value={color}
        onValueChange={setColor}
        xChannel="saturation"
        yChannel="brightness"
        aria-label="Saturation and brightness"
      />
      <ColorSlider channel="hue" value={color} onValueChange={setColor} />
      <ColorSlider channel="alpha" value={color} onValueChange={setColor} />
      <div className="flex items-center gap-3">
        <ColorSwatch color={color} />
        <span className="font-mono text-xs text-nowrap">
          {color.alpha < 1 ? formatColor(color, 'rgba') : formatColor(color, 'hex')}
        </span>
      </div>
    </div>
  )
}
```

### Output formats [#output-formats]

The area works in one space and hands you a `Color`; `formatColor` writes that out in any format you like, with no second control needed. Here the same pick is shown four ways at once - drag the thumb and every line updates.

```tsx
'use client'

import { useState } from 'react'
import { ColorArea } from '@appica/ui-react/color-area'
import { type Color, formatColor, parseColor } from '@appica/ui-react/color'

const FORMATS = ['hex', 'rgb', 'hsl', 'hsb'] as const

export default function ColorAreaFormats() {
  const [color, setColor] = useState<Color>(() => parseColor('#3b82f6'))

  return (
    <div className="flex w-64 flex-col gap-4">
      <ColorArea
        value={color}
        onValueChange={setColor}
        colorSpace="hsb"
        xChannel="saturation"
        yChannel="brightness"
        aria-label="Theme color"
        className="size-64"
      />
      <dl className="grid gap-1 text-xs">
        {FORMATS.map((format) => (
          <div key={format} className="flex gap-2">
            <dt className="text-foreground-muted w-8">{format}</dt>
            <dd className="text-foreground-intense font-mono">{formatColor(color, format)}</dd>
          </div>
        ))}
      </dl>
    </div>
  )
}
```

### Choosing channels [#choosing-channels]

Any two channels of the space can go on the axes. Here red runs horizontally and blue vertically, leaving green as the constant - move the thumb and only two of the three readouts change.

```tsx
'use client'

import { useState } from 'react'
import { ColorArea } from '@appica/ui-react/color-area'
import { type Color, formatChannelValue, parseColor } from '@appica/ui-react/color'

export default function ColorAreaChannels() {
  const [color, setColor] = useState<Color>(() => parseColor('rgb(59, 130, 246)'))

  return (
    <div className="flex flex-col gap-3">
      <ColorArea
        value={color}
        onValueChange={setColor}
        xChannel="red"
        yChannel="blue"
        aria-label="Red and blue"
        className="size-56"
      />
      <dl className="text-foreground-muted grid grid-cols-3 gap-2 text-xs tabular-nums">
        {(['red', 'green', 'blue'] as const).map((channel) => (
          <div key={channel} className="flex flex-col">
            <dt className="capitalize">{channel}</dt>
            <dd className="text-foreground-intense font-medium">{formatChannelValue(color, channel)}</dd>
          </div>
        ))}
      </dl>
    </div>
  )
}
```

### Sizing [#sizing]

The area is `size-56` (224px) by default and takes any size through `className`. The corner radius carries to the gradient and the ring, so a fully rounded area works too. `thumbProps` reaches the thumb, for a smaller one on a small area.

```tsx
import { ColorArea } from '@appica/ui-react/color-area'

export default function ColorAreaSizing() {
  return (
    <div className="flex w-full max-w-90 flex-col gap-4">
      <ColorArea
        defaultValue="hsb(280, 70%, 90%)"
        xChannel="saturation"
        yChannel="brightness"
        aria-label="Wide color area"
        className="h-32 w-full rounded-2xl"
      />
      <div className="flex items-end gap-4">
        <ColorArea
          defaultValue="hsb(140, 70%, 90%)"
          xChannel="saturation"
          yChannel="brightness"
          aria-label="Small color area"
          className="size-16 rounded-full"
          thumbProps={{ className: 'size-4' }}
        />
        <ColorArea
          defaultValue="hsb(30, 70%, 90%)"
          xChannel="saturation"
          yChannel="brightness"
          aria-label="Medium color area"
          className="size-24 rounded-xs"
        />
      </div>
    </div>
  )
}
```

### Forms [#forms]

`xName` and `yName` name the two hidden range inputs the component already renders for accessibility, so the axis values submit with a plain HTML form. No hidden field or `onSubmit` plumbing needed.

```tsx
'use client'

import { useState } from 'react'
import { ColorArea } from '@appica/ui-react/color-area'
import { Button } from '@appica/ui-react/button'

export default function ColorAreaForm() {
  const [submitted, setSubmitted] = useState<string | null>(null)

  return (
    <form
      className="flex flex-col items-start gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(`saturation ${data.get('saturation')}, brightness ${data.get('brightness')}`)
      }}
    >
      <ColorArea
        defaultValue="hsb(217, 76%, 96%)"
        xChannel="saturation"
        yChannel="brightness"
        xName="saturation"
        yName="brightness"
        aria-label="Brand color"
        className="size-40"
      />
      <Button type="submit" size="sm">
        Submit
      </Button>
      {submitted ? <p className="text-foreground-muted font-mono text-xs">{submitted}</p> : null}
    </form>
  )
}
```

### Disabled [#disabled]

`disabled` blocks interaction and swaps the gradient for a flat muted fill inside a dashed outline, dimmed.

```tsx
import { ColorArea } from '@appica/ui-react/color-area'

export default function ColorAreaDisabled() {
  return (
    <ColorArea
      defaultValue="hsb(217, 76%, 96%)"
      xChannel="saturation"
      yChannel="brightness"
      aria-label="Brand color"
      disabled
      className="size-40"
    />
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior (roving focus, popup placement, and the like) follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The horizontal axis mirrors: the x channel's minimum sits on the right edge, the gradient flows the same way, and &#x2A;*←/→** swap so the arrow that moves the thumb toward the maximum is still the one pointing at it. The vertical axis is unaffected. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).


## API reference [#api-reference]

### ColorArea [#colorarea]

| Prop               | Type    | Default          | Description                                                                                                                                                                                  |
| ------------------ | ---------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`            | `Color \| string`                   | -                | Selected color. Pass a `Color` or any CSS color string to control the component. Inside a `ColorPicker` it can be left off: the control then reads and writes the picker's color.                                                   |
| `defaultValue`     | `Color \| string`                   | `'#ffffff'`      | Color selected before any interaction, when the component is uncontrolled.                                                                                                                                                          |
| `onValueChange`    | `(value: Color) => void`            | -                | Fires on every change, including each frame of a drag.                                                                                                                                                                              |
| `onValueCommitted` | `(value: Color) => void`            | -                | Fires once a drag ends or a key press settles, with the color that was landed on.                                                                                                                                                   |
| `colorSpace`       | `'rgb' \| 'hsl' \| 'hsb'`           | value's space    | Color space the axes operate in. Defaults to the space of the value, so a color parsed from `hsl(...)` gives an HSL area and a hex one gives an RGB area. The space is independent of the format you store: convert on the way out. |
| `xChannel`         | `ColorChannel`                                 | first free       | Channel mapped to the horizontal axis. Defaults to the first channel of the color space that `yChannel` has not taken.                                                                                                              |
| `yChannel`         | `ColorChannel`                                 | first free       | Channel mapped to the vertical axis, increasing upwards. Defaults to the first channel of the color space left free by `xChannel`.                                                                                                  |
| `disabled`         | `boolean`                                      | `false`          | Prevent interaction and dim the area.                                                                                                                                                                                               |
| `xName`            | `string`                                       | -                | Name of the hidden horizontal input, used when submitting an HTML form.                                                                                                                                                             |
| `yName`            | `string`                                       | -                | Name of the hidden vertical input, used when submitting an HTML form.                                                                                                                                                               |
| `form`             | `string`                                       | -                | `id` of the `<form>` the hidden inputs belong to, when they sit outside it.                                                                                                                                                         |
| `thumbProps`       | `ComponentPropsWithoutRef\<'span'>` | -                | Props for the thumb element, for styling or a test id.                                                                                                                                                                              |
| `aria-label`       | `string`                                       | `'Color picker'` | Accessible name, read before the channel values.                                                                                                                                                                                    |
| `className`        | `string`                                       | -                | Extra classes on the root, merged via `tailwind-merge`.                                                                                                                                                                             |

Every other `<div>` attribute is forwarded to the root. The root carries `data-space`, `data-disabled` and `data-dragging`; the parts are addressable through `data-slot` (`color-area`, `color-area-surface`, `color-area-thumb`).

### Color utilities [#color-utilities]

The color model ships from its own subpath, `@appica/ui-react/color`, and is shared by every color component.

```tsx
import { parseColor, formatColor, convertColor } from '@appica/ui-react/color'
```

| Export                                  | Signature                    | Description                                                                        |
| --------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `Color`                                 | `RgbColor \| HslColor \| HsbColor \| OklchColor`         | A color value. A plain object with a `space` tag, its three channels, and `alpha`.                                        |
| `ColorSpace`                            | `'rgb' \| 'hsl' \| 'hsb' \| 'oklch'`                     | A supported color space.                                                                                                  |
| `ColorChannel`                          | `'red' \| 'green' \| 'blue' \| 'hue' \| … \| 'alpha'`    | A single adjustable component of a color.                                                                                 |
| `parseColor`                            | `(value: string) => Color`                               | Parses hex (3, 4, 6 or 8 digits), `rgb()`, `hsl()`, `oklch()` and `hsb()`, comma or space separated. Throws on bad input. |
| `safeParseColor`                        | `(value: string) => Color \| undefined`                  | Same, returning `undefined` instead of throwing.                                                                          |
| `formatColor`                           | `(color, format?) => string`                             | Serializes to a CSS string. Defaults to `'css'`, the shortest form for the color's own space.                             |
| `convertColor`                          | `(color, space) => Color`                                | Converts into another space, through sRGB.                                                                                |
| `getChannelValue` / `withChannelValue`  | `(color, channel\[, value]) => number \| Color`          | Reads a channel, or returns a copy with it clamped and set.                                                               |
| `getChannelRange`                       | `(space, channel) => ColorChannelRange`                  | Bounds and increments of a channel: `minValue`, `maxValue`, `step`, `pageSize`.                                           |
| `getColorChannels`                      | `(space) => \[ColorChannel, ColorChannel, ColorChannel]` | The three channels of a space, in canonical order.                                                                        |
| `getColorSpaceAxes`                     | `(space, axes?) => ColorAxes`                            | Resolves `xChannel`, `yChannel` and the leftover `zChannel`.                                                              |
| `getChannelName` / `formatChannelValue` | `(channel) => string`                                    | Display name of a channel, and a channel's value with its unit.                                                           |

## Accessibility [#accessibility]

* The root is a `role="group"` holding two visually hidden `<input type="range">`, one per axis, so assistive technology gets a real value, bounds, and step for each channel. Only the focused axis is in the tab order.
* `aria-valuetext` on both inputs spells out all three channels of the space, so the constant channel is announced too. `aria-roledescription` marks the control as a two-dimensional slider.
* Keyboard: &#x2A;*←/→*&#x2A; move the x channel and &#x2A;*↑/↓** the y channel by one step, **Shift** with an arrow moves by the page step, **Page Up·Down** move the y channel by the page step, and **Home/End** move the x channel by the page step.
* The thumb is a white rim with a faint dark ring on each side of it, so it keeps an edge over any point of the plane in either theme - including when the picked color is itself white. `forced-color-adjust: none` keeps the gradient intact in forced-colors mode.
* The thumb's position is never transitioned, so it sits exactly under the pointer. The only animation is the scale while it is pressed, and that honors `prefers-reduced-motion`.
