Skip to content

ColorArea

A rectangular 2D area component for adjusting two color channels mapped to the horizontal and vertical axes.

Preview

Source code
vue
<script setup lang="ts">
import {
  ColorAreaRoot,
  ColorAreaArea,
  ColorAreaGradient,
  ColorAreaThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorAreaRoot
    v-model="color"
    color-space="hsl"
    x-channel="h"
    y-channel="s"
    as="div"
    class="
      relative block h-[200px] w-full cursor-crosshair touch-none overflow-clip
      rounded-lg
    "
    aria-label="HSL color area"
  >
    <ColorAreaArea as="div" class="absolute inset-0">
      <ColorAreaGradient
        as="div"
        class="absolute inset-0"
      />
      <ColorAreaThumb
        as="div"
        class="
          absolute size-5 transform-(--reka-slider-area-thumb-transform)
          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)]
        "
      />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

Anatomy

vue
<template>
  <ColorAreaRoot>
    <ColorAreaArea>
      <ColorAreaGradient />
      <ColorAreaThumb />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

WARNING

ColorAreaArea is required. ColorAreaRoot owns the state but attaches no pointer or keyboard handlers of its own: they all live on ColorAreaArea, which also measures the box that pointer coordinates are resolved against. A tree that puts the gradient and thumb directly under the root renders correctly but does not respond to input.

Examples

HSL

HSL color area with Hue on X and Saturation on Y.

Source code
vue
<script setup lang="ts">
import {
  ColorAreaRoot,
  ColorAreaArea,
  ColorAreaGradient,
  ColorAreaThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorAreaRoot
    v-model="color"
    color-space="hsl"
    x-channel="h"
    y-channel="s"
    as="div"
    class="
      relative block h-[200px] w-full cursor-crosshair touch-none overflow-clip
      rounded-lg
    "
    aria-label="HSL color area"
  >
    <ColorAreaArea as="div" class="absolute inset-0">
      <ColorAreaGradient
        as="div"
        class="absolute inset-0"
      />
      <ColorAreaThumb
        as="div"
        class="
          absolute size-5 transform-(--reka-slider-area-thumb-transform)
          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)]
        "
      />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

OKLCh

OKLCh color area with Chroma on X and Lightness on Y.

Source code
vue
<script setup lang="ts">
import {
  ColorAreaRoot,
  ColorAreaArea,
  ColorAreaGradient,
  ColorAreaThumb,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorAreaRoot
    v-model="color"
    color-space="oklch"
    x-channel="c"
    y-channel="l"
    as="div"
    class="
      relative block h-[200px] w-full cursor-crosshair touch-none overflow-clip
      rounded-lg
    "
    aria-label="OKLCh color area"
  >
    <ColorAreaArea as="div" class="absolute inset-0">
      <ColorAreaGradient
        as="div"
        class="absolute inset-0"
      />
      <ColorAreaThumb
        as="div"
        class="
          absolute size-5 transform-(--reka-slider-area-thumb-transform)
          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)]
        "
      />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

With Alpha

Pass :channel-overrides="false" on ColorAreaGradient to reflect the color's alpha channel as opacity on the gradient. ColorAreaGradient paints a checkerboard behind the canvas automatically, so transparency is visible with no extra element.

vue
<template>
  <ColorAreaRoot
    v-model="color"
    color-space="hsl"
    x-channel="s"
    y-channel="l"
  >
    <ColorAreaArea>
      <ColorAreaGradient :channel-overrides="false" />
      <ColorAreaThumb />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

Usage

vue
<script setup lang="ts">
import {
  useColor,
  ColorAreaRoot,
  ColorAreaArea,
  ColorAreaGradient,
  ColorAreaThumb,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
</script>

<template>
  <ColorAreaRoot
    v-model="color"
    color-space="hsl"
    x-channel="h"
    y-channel="s"
    as="div"
  >
    <ColorAreaArea as="div">
      <ColorAreaGradient as="div" />
      <ColorAreaThumb as="div" />
    </ColorAreaArea>
  </ColorAreaRoot>
</template>

API Reference

ColorAreaRoot

The root container that owns the color state, the channel maths and the keyboard handler. Renders role="group".

PropTypeDefaultDescription
modelValueColor | string | nullControlled color value (v-model).
defaultValueColor | string'hsl(0, 100%, 50%)'Initial color when uncontrolled.
colorSpaceSpaceId'hsl'Color space (e.g. 'hsl', 'oklch').
xChannelstringAutoChannel for the X axis (e.g. 's', 'alpha'). Defaults to the color space's first channel.
yChannelstringAutoChannel for the Y axis (e.g. 'l', 'alpha'). Defaults to the color space's second channel.
xNamestringName of a hidden input carrying the raw X channel value for form submission.
yNamestringName of a hidden input carrying the raw Y channel value for form submission.
disabledbooleanfalseDisables interaction.
dir'ltr' | 'rtl'Reading direction. Inherits from ConfigProvider when omitted.
xInvertedbooleanfalseInvert X axis.
yInvertedbooleanfalseInvert Y axis.
minXStepsBetweenThumbsnumber0Minimum permitted steps between thumbs on the X axis.
minYStepsBetweenThumbsnumber0Minimum permitted steps between thumbs on the Y axis.
thumbAlignment'contain' | 'overflow''overflow'Whether thumbs are kept inside the track bounds.
namestringHidden input name carrying the full color for form submission.
requiredbooleanMarks the hidden input as required for form submission.
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

TIP

ColorArea currently renders a single thumb, so minXStepsBetweenThumbs and minYStepsBetweenThumbs have no observable effect.

The root publishes --reka-slider-area-thumb-transform in its own style, which is the centring transform the thumb consumes. dir and x-inverted each mirror the X axis, so setting both cancels out. The hidden name, x-name and y-name inputs are only rendered when the root is inside a <form>.

EventPayloadDescription
update:modelValueColor | undefinedEmitted whenever the color changes.
update:colorColorMirrors update:modelValue; present for API parity.
changeColorEmitted on every value change, including mid-drag.
changeEndColorEmitted when a change-producing interaction ends.
SlotPayloadDescription
default{ modelValue: Color | undefined }The area's parts, with the current color exposed as a slot prop.

ColorAreaArea

The interaction surface. Renders role="application" with aria-roledescription="Color picker" and touch-action: none, registers itself as the element pointer coordinates are measured against, and carries the pointer and keyboard listeners. It must wrap ColorAreaGradient and ColorAreaThumb.

PropTypeDefaultDescription
asstring'div'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

Building a custom interaction surface

ColorAreaRoot's context exposes handleSlideStart, handleSlideMove, handleSlideEnd and snapshotValues, so a custom surface can drive the same value maths. It cannot, however, register itself as the measured area. Only ColorAreaArea writes areaElement. So pointer coordinates would be resolved against the root's box rather than the custom surface's. Reimplement ColorAreaArea (rather than wrapping it) if the two boxes differ.

ColorAreaGradient

Renders the area's 2D gradient, sampled from the root's color space and channel configuration, over a checkerboard so alpha transparency is visible without a separate element.

Any pair of hsv's or hsl's own three channels has an exact CSS equivalent, as do explicit corner colors and any axis bound to alpha, all of those render no <canvas> at all. A two-channel area in a perceptual space (oklch, oklab, lab, lch), in hwb, or in the RGB family keeps the canvas, as does corner mode with an interpolationSpace set.

PropTypeDefaultDescription
renderer'auto' | 'css' | 'canvas''auto'Which painter to use. 'auto' paints with stacked CSS gradients when an exact recipe exists for the color space and channels, and falls back to the canvas otherwise. 'css' forces the CSS path and warns in development if no recipe exists. 'canvas' always paints into a <canvas>.
topLeftstringOverride: color for the top-left corner.
topRightstringOverride: color for the top-right corner.
bottomLeftstringOverride: color for the bottom-left corner.
bottomRightstringOverride: color for the bottom-right corner.
interpolationSpaceSpaceIdColor space for perceptual interpolation (e.g. 'oklch'). Switches the corner-color path from WebGL to 2D canvas.
channelOverridesRecord<string, number> | false{ alpha: 1 }Lock specific channels to fixed values in the gradient. Set to false to reflect all channels from the current color including alpha. E.g. { s: 1, v: 1, alpha: 1 } for an immutable hue gradient in HSV.
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

ColorAreaCheckerboard deprecated

Deprecated

ColorAreaGradient now paints the checkerboard itself, so this component is no longer needed and is kept only for backwards compatibility. It emits a one-time console warning in development. To render a checkerboard elsewhere, apply a CSS repeating-conic-gradient background to your own element.

Renders a checkerboard pattern behind the gradient to visualize alpha transparency. Place it inside ColorAreaArea before ColorAreaGradient.

PropTypeDefaultDescription
asstring'div'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

ColorAreaThumb

The single combined handle, and the area's only focusable element. One thumb drives both axes: it renders role="slider", takes tabindex="0" unless the root is disabled, and is positioned absolutely from the X and Y channel values. It reads the --reka-slider-area-thumb-transform custom property set by the root for its centering transform.

Because one handle serves two channels, it announces both: aria-label names the channel pair and aria-valuetext carries both formatted values. There is no separate thumb per axis. The thumb is only a focus target and an ARIA surface; every value change is owned by ColorAreaArea, whose keydown listener sees the events that bubble up from here.

PropTypeDefaultDescription
asstring'span'The element or component to render as.
asChildbooleanfalseMerge props onto the single child instead of rendering an element.

An aria-label passed as a plain attribute wins over the generated channel-pair label. A mirrored axis is anchored from the opposite edge, so the thumb sets right/bottom instead of left/top and the percentage stays positive; with thumb-alignment="contain" a pixel offset is added so the handle never overhangs the box.

Data Attributes

AttributePartPresent when
data-disabledRoot, Area, Gradient, ThumbThe root is disabled.

CSS Variables

The transparency grid reads three custom properties and no component writes them, so a rule anywhere above the element wins:

VariableDefaultDescription
--urcolor-checkerboard-darkrgb(230, 230, 230)The darker of the two checks.
--urcolor-checkerboard-lightwhiteThe lighter of the two checks.
--urcolor-checkerboard-size16pxThe tile size, applied to both axes.

The grid is a single background shorthand, so an invalid value invalidates the whole declaration rather than its own layer. Keep overrides to a <color> and a <length>.

Accessibility

ColorArea exposes a single focusable thumb inside an application-role surface, with keyboard access to both axes.

ARIA Labels

AttributeDescription
role="group"Applied to ColorAreaRoot.
role="application"Applied to ColorAreaArea, with aria-roledescription="Color picker".
role="slider"Applied to ColorAreaThumb, with aria-roledescription="Color thumb".
aria-labelDefaults to the two channel labels, e.g. "Hue, Saturation". Override with your own aria-label on the thumb.
aria-valuemin / aria-valuemaxThe X channel's range.
aria-valuenowThe current X channel value. Only one number can be carried here, so the X axis owns it.
aria-valuetextBoth channels formatted, e.g. "Hue 210°, Saturation 80%".
aria-disabledApplied to ColorAreaRoot and ColorAreaArea when disabled is set.

Keyboard Navigation

KeyAction
Arrow Right / Arrow LeftMove one step along the X axis
Arrow Down / Arrow UpMove one step along the Y axis
Shift + ArrowMove by 10 steps
Home / EndJump to the left / right edge of the X axis
Page Up / Page DownJump to the top / bottom edge of the Y axis

Keys address the visual axes: with x-inverted or y-inverted set, or in RTL, the direction of travel flips so the thumb still moves the way the key points. Each key press that changes the value emits changeEnd as well as change.