Skip to content

ColorArea

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

Preview

Every part ships as a standalone attribute directive, so you own each element. COLOR_AREA_DIRECTIVES brings the whole family in with one entry in imports.

ts
import { Component, signal } from "@angular/core";
import { Color } from "@urcolor/core";
import { COLOR_AREA_DIRECTIVES } from "@urcolor/angular";

@Component({
  selector: "my-area",
  imports: [...COLOR_AREA_DIRECTIVES],
  template: `
    <div
      urcColorAreaRoot
      [(value)]="color"
      colorSpace="hsl"
      xChannel="h"
      yChannel="s"
      class="relative block h-[200px] w-full cursor-crosshair touch-none overflow-clip rounded-lg"
    >
      <canvas urcColorAreaGradient class="absolute inset-0"></canvas>
      <div urcColorAreaThumb class="absolute size-5 rounded-full border-2 border-white"></div>
    </div>
  `,
})
export class MyArea {
  protected readonly color = signal<Color>(Color.parse("hsl(210, 80%, 50%)")!);
}

Anatomy

html
<div urcColorAreaRoot>
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

The gradient's selector is canvas[urcColorAreaGradient], so it must sit on a <canvas> element. The root and thumb selectors are element-agnostic. The root is the interaction surface: pointer capture, the keyboard listener and the box that pointer coordinates are measured against all live on it, so the gradient and the thumb sit directly inside it.

Examples

HSL / Hue x Saturation

HSL color area with Hue on the horizontal axis and Saturation on the vertical axis. Both channels are the color space's defaults, so they can be omitted.

html
<div
  urcColorAreaRoot
  [(value)]="color"
  colorSpace="hsl"
  xChannel="h"
  yChannel="s"
  class="relative block h-[200px] w-full touch-none overflow-clip rounded-lg"
>
  <canvas urcColorAreaGradient class="absolute inset-0"></canvas>
  <div urcColorAreaThumb class="absolute size-5 rounded-full border-2 border-white"></div>
</div>

OKLCh / Chroma x Lightness

OKLCh color area with Chroma on the horizontal axis and Lightness on the vertical axis.

html
<div
  urcColorAreaRoot
  [(value)]="color"
  colorSpace="oklch"
  xChannel="c"
  yChannel="l"
  class="relative block h-[200px] w-full touch-none overflow-clip rounded-lg"
>
  <canvas urcColorAreaGradient class="absolute inset-0"></canvas>
  <div urcColorAreaThumb class="absolute size-5 rounded-full border-2 border-white"></div>
</div>

Alpha on an axis

Map yChannel to "alpha" to make the vertical axis drive opacity. The canvas is painted with real transparency and composites over its own checkerboard background.

html
<div urcColorAreaRoot [(value)]="color" colorSpace="hsl" xChannel="h" yChannel="alpha">
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

Explicit corner colors

Supplying any of topLeft, topRight, bottomLeft or bottomRight switches the gradient to corner mode and ignores the channel sampling. Adding interpolationSpace swaps the WebGL path for a CPU one that interpolates perceptually in that space.

html
<div urcColorAreaRoot [(value)]="color">
  <canvas
    urcColorAreaGradient
    topLeft="white"
    topRight="oklch(0.7 0.2 30)"
    bottomLeft="black"
    bottomRight="black"
    interpolationSpace="oklch"
  ></canvas>
  <div urcColorAreaThumb></div>
</div>

Commit, disabled and direction

disabled and dir are native attributes, not inputs: set them on the element and the root picks them up. (valueCommit) fires once at the end of an interaction, never mid-drag.

html
<div urcColorAreaRoot [(value)]="color" (valueCommit)="onCommit($event)" dir="rtl" disabled>
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

Inverted axes

xInverted and yInverted are booleanAttribute inputs, so the bare attribute form works. They mirror an axis, and the gradient, the thumb position and the arrow keys all follow.

html
<div urcColorAreaRoot [(value)]="color" yInverted>
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

Template reference

Every directive sets exportAs, so the root's state is readable from the template.

html
<div urcColorAreaRoot #area="urcColorAreaRoot" [(value)]="color">
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

<p>{{ area.xChannel() }}: {{ area.valueX() }} / {{ area.yChannel() }}: {{ area.valueY() }}</p>

Signal Forms

ColorAreaRoot implements FormValueControl<Color>, so it binds straight to a signal form field.

html
<div urcColorAreaRoot [field]="form.brandColor">
  <canvas urcColorAreaGradient></canvas>
  <div urcColorAreaThumb></div>
</div>

API Reference

COLOR_AREA_DIRECTIVES is the array of every part below. COLOR_AREA_DEFAULT_COLOR is the Color a root falls back to when [(value)] is never bound.

ColorAreaRoot

The root of the area. Owns the color, the pointer and keyboard interaction, and every piece of state the other parts read through inject(ColorAreaRoot).

  • Selector: [urcColorAreaRoot]
  • Export as: urcColorAreaRoot
  • Implements: FormValueControl<Color>
PropTypeDefaultDescription
valuemodel<Color>hsl(0, 100%, 50%)The color, two-way bindable as [(value)]. Also the Signal Forms contract.
colorSpaceinput<SpaceId>'hsl'The color space both axes operate in.
xChannelinput<string | undefined>AutoChannel driven by the horizontal axis, or 'alpha'. Defaults to the color space's first channel.
yChannelinput<string | undefined>AutoChannel driven by the vertical axis, or 'alpha'. Defaults to the color space's second channel.
xInvertedinput<boolean>falseRuns the horizontal axis opposite to its natural direction. Coerced with booleanAttribute.
yInvertedinput<boolean>falseRuns the vertical axis opposite to its natural direction. Coerced with booleanAttribute.
thumbAlignmentinput<ColorAreaThumbAlignment>'overflow'Whether the thumb straddles the edge ('overflow') or is pulled fully inside it.
valueCommitoutput<Color>Emitted once at the end of an interaction, never mid-drag.

ColorAreaThumbAlignment is "contain" | "overflow".

disabled is not an input

disabled is the native DOM attribute. The static attribute is read at construction, which works under SSR, and a MutationObserver keeps it live afterwards. The readable signal is named isDisabled, not disabled, because FormUiControl reserves the member name disabled for its own InputSignal<boolean>.

dir is DOM state too, and is read by the same observer. The resolved value comes from getComputedStyle(...).direction rather than the host attribute alone, so an inherited <html dir="rtl"> is honoured. It is published as the dir signal. RTL and xInverted each mirror the horizontal axis, so setting both cancels out.

Readable signals, for exportAs template references and for inject(ColorAreaRoot):

MemberTypeDescription
isDisabledSignal<boolean>Whether interaction is refused.
dirSignal<'ltr' | 'rtl'>The resolved reading direction.
draggingSignal<boolean>True while a pointer drag is in flight.
xChannelSignal<string>The resolved horizontal channel, after the space's default is applied.
yChannelSignal<string>The resolved vertical channel, after the space's default is applied.
valueX / valueYSignal<number>The two channels in display units.
minX / maxXSignal<number>Bounds of the horizontal axis, in display units.
minY / maxYSignal<number>Bounds of the vertical axis, in display units.
isSlidingFromLeftSignal<boolean>False when the horizontal axis is mirrored.
isSlidingFromTopSignal<boolean>False when the vertical axis is mirrored.

TIP

The root publishes --reka-slider-area-thumb-transform on its own style, which is the centring transform the thumb consumes.

ColorAreaGradient

Paints the area's two-dimensional color surface onto a <canvas> you supply. The transparency checkerboard is this element's CSS background, which the canvas bitmap composites over. There is no Checkerboard part in this package.

  • Selector: canvas[urcColorAreaGradient]
  • Export as: urcColorAreaGradient
PropTypeDefaultDescription
topLeftinput<string | undefined>Explicit top-left corner color. Supplying any corner switches the directive to corner mode.
topRightinput<string | undefined>Explicit top-right corner color.
bottomLeftinput<string | undefined>Explicit bottom-left corner color.
bottomRightinput<string | undefined>Explicit bottom-right corner color.
interpolationSpaceinput<SpaceId | undefined>Interpolate the corner surface in this space for perceptual accuracy. Switches the corner path from WebGL to a CPU sampler.
channelOverridesinput<ColorAreaChannelOverrides>{ alpha: 1 }Lock specific channels to fixed values in the gradient. Pass false for no overrides at all.

ColorAreaChannelOverrides is Record<string, number> | false.

ColorAreaThumb

The single combined handle, and the area's only focusable element. One thumb drives both axes: it sets role="slider", takes tabindex="0" unless the root is disabled, and is positioned from the horizontal and vertical channel values.

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 the root, whose keydown listener sees the events that bubble up from here.

  • Selector: [urcColorAreaThumb]
  • Export as: urcColorAreaThumb

The directive takes no inputs. A static aria-label on the host element is read at construction and wins over the generated channel-pair label. A mirrored axis is anchored from the opposite edge, so the directive sets right/bottom instead of left/top and the percentage stays positive.

Data Attributes

AttributePartPresent when
data-slider-area-implRootAlways.
data-disabledRoot, ThumbThe root is disabled.
data-draggingRoot, ThumbA pointer drag is in flight.

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 that drives both the horizontal and the vertical channel. Keyboard events are handled on the root, which sees them bubble up from the focused thumb.

ARIA Labels

AttributeDescription
role="slider"Applied to urcColorAreaThumb, with aria-roledescription="2D slider".
aria-labelDefaults to the two channel labels joined with "and", e.g. "Hue and Saturation". Set your own aria-label on the thumb element to override.
aria-valuemin / aria-valuemaxThe horizontal channel's range.
aria-valuenowThe current horizontal channel value. Only one number can be carried here, so the X axis owns it.
aria-valuetextBoth channels formatted with their units, e.g. "210°, 80%".
aria-disabledApplied to the root and the thumb when the native disabled attribute is set.

Keyboard Navigation

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

Keys address the visual axes: xInverted, yInverted and RTL flip the direction of travel so the thumb still moves the way the key points. (valueCommit) fires once on key release, not on every repeat.