Skip to content

ColorField

A numeric input component for editing individual color channels, with optional increment/decrement buttons and a color swatch preview.

Preview

Source code
vue
<script setup lang="ts">
import { computed } from "vue";
import "internationalized-color/css";
import { colorSpaces } from "@urcolor/core";
import { Label } from "reka-ui";
import {
  ColorFieldRoot,
  ColorFieldInput,
  ColorFieldIncrement,
  ColorFieldDecrement,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
const channels = computed(() => colorSpaces["hsl"]?.channels ?? []);
</script>

<template>
  <div class="flex items-start gap-4">
    <div class="flex flex-1 flex-wrap gap-2">
      <div
        v-for="ch in channels"
        :key="ch.key"
        class="flex min-w-[80px] flex-1 flex-col gap-1"
      >
        <Label
          :for="`field-${ch.key}`"
          class="text-xs font-semibold text-(--vp-c-text-2)"
        >{{ ch.label }}</Label>
        <ColorFieldRoot
          v-model="color"
          color-space="hsl"
          :channel="ch.key"
          class="
            flex items-center overflow-hidden rounded-md border
            border-(--vp-c-divider) bg-(--vp-c-bg)
          "
        >
          <ColorFieldDecrement
            class="
              flex size-8 shrink-0 cursor-pointer items-center justify-center
              border-r border-none border-r-(--vp-c-divider) bg-transparent
              text-lg leading-none text-(--vp-c-text-2) select-none
              hover:not-disabled:bg-(--vp-c-bg-soft)
              hover:not-disabled:text-(--vp-c-text-1)
              disabled:cursor-default disabled:opacity-30
            "
          >
            &minus;
          </ColorFieldDecrement>
          <ColorFieldInput
            :id="`field-${ch.key}`"
            class="
              w-0 min-w-0 flex-1 border-none bg-transparent px-0.5 py-1
              text-center font-mono text-[13px] text-(--vp-c-text-1)
              outline-none
            "
          />
          <ColorFieldIncrement
            class="
              flex size-8 shrink-0 cursor-pointer items-center justify-center
              border-l border-none border-l-(--vp-c-divider) bg-transparent
              text-lg leading-none text-(--vp-c-text-2) select-none
              hover:not-disabled:bg-(--vp-c-bg-soft)
              hover:not-disabled:text-(--vp-c-text-1)
              disabled:cursor-default disabled:opacity-30
            "
          >
            +
          </ColorFieldIncrement>
        </ColorFieldRoot>
      </div>
    </div>
  </div>
</template>

Anatomy

vue
<template>
  <ColorFieldRoot>
    <ColorFieldDecrement />
    <ColorFieldInput />
    <ColorFieldIncrement />
  </ColorFieldRoot>

  <ColorFieldSwatch />
</template>

Examples

Hex Input

Source code
vue
<script setup lang="ts">
import "internationalized-color/css";
import {
  ColorFieldRoot,
  ColorFieldInput,
  useColor,
} from "@urcolor/vue";

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

<template>
  <div class="flex items-center gap-3">
    <ColorFieldRoot
      v-model="color"
      color-space="hex"
      channel="hex"
      format="hex"
      class="
        flex h-8 items-center overflow-hidden rounded-md border
        border-(--vp-c-divider) bg-(--vp-c-bg) px-3
      "
    >
      <ColorFieldInput
        class="
          min-w-0 flex-1 border-none bg-transparent px-3 py-1.5 font-mono
          text-[13px] text-(--vp-c-text-1) outline-none
        "
      />
    </ColorFieldRoot>
  </div>
</template>

HSL Channel Fields

HSL channel inputs with increment/decrement buttons.

Source code
vue
<script setup lang="ts">
import { computed } from "vue";
import "internationalized-color/css";
import { colorSpaces } from "@urcolor/core";
import { Label } from "reka-ui";
import {
  ColorFieldRoot,
  ColorFieldInput,
  ColorFieldIncrement,
  ColorFieldDecrement,
  useColor,
} from "@urcolor/vue";

const { color } = useColor("hsl(210, 80%, 50%)");
const channels = computed(() => colorSpaces["hsl"]?.channels ?? []);
</script>

<template>
  <div class="flex items-start gap-4">
    <div class="flex flex-1 flex-wrap gap-2">
      <div
        v-for="ch in channels"
        :key="ch.key"
        class="flex min-w-[80px] flex-1 flex-col gap-1"
      >
        <Label
          :for="`field-${ch.key}`"
          class="text-xs font-semibold text-(--vp-c-text-2)"
        >{{ ch.label }}</Label>
        <ColorFieldRoot
          v-model="color"
          color-space="hsl"
          :channel="ch.key"
          class="
            flex items-center overflow-hidden rounded-md border
            border-(--vp-c-divider) bg-(--vp-c-bg)
          "
        >
          <ColorFieldDecrement
            class="
              flex size-8 shrink-0 cursor-pointer items-center justify-center
              border-r border-none border-r-(--vp-c-divider) bg-transparent
              text-lg leading-none text-(--vp-c-text-2) select-none
              hover:not-disabled:bg-(--vp-c-bg-soft)
              hover:not-disabled:text-(--vp-c-text-1)
              disabled:cursor-default disabled:opacity-30
            "
          >
            &minus;
          </ColorFieldDecrement>
          <ColorFieldInput
            :id="`field-${ch.key}`"
            class="
              w-0 min-w-0 flex-1 border-none bg-transparent px-0.5 py-1
              text-center font-mono text-[13px] text-(--vp-c-text-1)
              outline-none
            "
          />
          <ColorFieldIncrement
            class="
              flex size-8 shrink-0 cursor-pointer items-center justify-center
              border-l border-none border-l-(--vp-c-divider) bg-transparent
              text-lg leading-none text-(--vp-c-text-2) select-none
              hover:not-disabled:bg-(--vp-c-bg-soft)
              hover:not-disabled:text-(--vp-c-text-1)
              disabled:cursor-default disabled:opacity-30
            "
          >
            +
          </ColorFieldIncrement>
        </ColorFieldRoot>
      </div>
    </div>
  </div>
</template>

Alpha Channel

Set channel="alpha" to create an opacity input (0–100%).

vue
<template>
  <ColorFieldRoot
    :model-value="color"
    color-space="hsl"
    channel="alpha"
    @update:model-value="onColorUpdate"
  >
    <ColorFieldDecrement>&minus;</ColorFieldDecrement>
    <ColorFieldInput />
    <ColorFieldIncrement>+</ColorFieldIncrement>
  </ColorFieldRoot>
</template>

API Reference

ColorFieldRoot

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

PropTypeDefaultDescription
modelValueColor | string | nullControlled color value (v-model).
colorSpacestring'hsl'Color space mode (e.g. 'hsl', 'oklch', 'hex').
channelstring'h'Channel to control (e.g. 'h', 's', 'l', 'hex').
format'number' | 'degree' | 'percentage' | 'hex'AutoDisplay format. Auto-derived from channel config if omitted.
minnumberAutoMinimum value. Auto-derived from channel config.
maxnumberAutoMaximum value. Auto-derived from channel config.
stepnumberAutoArrow key step increment. Auto-derived from channel config.
disabledbooleanfalseDisables interaction.
readOnlybooleanfalseMakes the field read-only.
namestringHidden input name for form submission.
requiredbooleanfalseMarks as required for form submission.
EventPayloadDescription
update:modelValueColor | undefinedEmitted when color changes.
valueCommitColorEmitted when interaction ends (blur or Enter).

ColorFieldInput

The text input element for entering color channel values.

ColorFieldIncrement

Button to increment the color field value. Auto-disabled at maximum. Supports press-and-hold for continuous increment.

PropTypeDefaultDescription
disabledbooleanfalseDisables the button.

ColorFieldDecrement

Button to decrement the color field value. Auto-disabled at minimum. Supports press-and-hold for continuous decrement.

PropTypeDefaultDescription
disabledbooleanfalseDisables the button.

ColorFieldSwatch

Displays a color preview swatch. Wraps ColorSwatchRoot with automatic checkerboard background for alpha visualization.

PropTypeDefaultDescription
modelValueColor | string | nullThe color value to display.
alphabooleanfalseWhen true, reflects the color's alpha channel.
checkerSizenumber16The checkerboard tile size in pixels.

Accessibility

ColorField provides a spinbutton interface for precise numeric color channel editing with keyboard-driven increment/decrement controls.

ARIA Labels

AttributeDescription
role="spinbutton"Applied to the input element for screen reader recognition.
aria-labelLabels the input with the channel name.
aria-valuemin / aria-valuemaxDefines the channel's value range.
aria-valuenowCurrent numeric value of the channel.

Keyboard Navigation

KeyAction
Arrow UpIncrease by one step
Arrow DownDecrease by one step
Page UpIncrease by 10x step
Page DownDecrease by 10x step
HomeJump to minimum
EndJump to maximum
EnterCommit current value