Build Color Fields
Let's build numeric input fields for editing individual color channels step by step.
Here's what we'll end up with:
Click to view the full code
<script setup lang="ts">
import { computed } from "vue";
import "internationalized-color/css";
import { colorSpaces } from "@urcolor/core";
import { Label } from "reka-ui";
import {
useColor,
ColorFieldRoot,
ColorFieldInput,
ColorFieldIncrement,
ColorFieldDecrement,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
const channels = computed(() => colorSpaces["hsl"]?.channels ?? []);
</script>
<template>
<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="`guide-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-none 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
"
>
−
</ColorFieldDecrement>
<ColorFieldInput
:id="`guide-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-none 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>
</template>Step 1: Set up state
Start by importing the color model and creating a reactive color value.
<script setup lang="ts">
import { useColor } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>Step 2: Add the root
ColorFieldRoot manages the state for a single channel input. Tell it which color space and channel to control.
<script setup lang="ts">
import { useColor, ColorFieldRoot } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorFieldRoot
v-model="color"
color-space="hsl"
channel="h"
>
<!-- children go here -->
</ColorFieldRoot>
</template>color-space— the color space to work in (hsl,oklch,hsb, etc.)channel— the channel this field controls (h,s,l, etc.)
Step 3: Add the input
ColorFieldInput renders the numeric input. It automatically formats the value based on the channel (degrees, percentages, etc.).
<script setup lang="ts">
import {
useColor,
ColorFieldRoot,
ColorFieldInput,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorFieldRoot
v-model="color"
color-space="hsl"
channel="h"
class="
flex items-center overflow-hidden rounded-md border
border-(--vp-c-divider) bg-(--vp-c-bg)
"
>
<ColorFieldInput
class="
w-full border-none bg-transparent px-2 py-1
text-center font-mono text-sm outline-none
"
/>
</ColorFieldRoot>
</template>The input supports keyboard interactions — arrow keys increment and decrement the value, and typing a number updates the color directly.
Step 4: Add increment and decrement buttons
ColorFieldIncrement and ColorFieldDecrement provide stepper buttons for fine-tuning the value.
<script setup lang="ts">
import {
useColor,
ColorFieldRoot,
ColorFieldInput,
ColorFieldIncrement,
ColorFieldDecrement,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorFieldRoot
v-model="color"
color-space="hsl"
channel="h"
class="
flex items-center overflow-hidden rounded-md border
border-(--vp-c-divider) bg-(--vp-c-bg)
"
>
<ColorFieldDecrement class="flex size-8 items-center justify-center">
−
</ColorFieldDecrement>
<ColorFieldInput
class="
w-0 min-w-0 flex-1 border-none bg-transparent px-0.5 py-1
text-center font-mono text-[13px] outline-none
"
/>
<ColorFieldIncrement class="flex size-8 items-center justify-center">
+
</ColorFieldIncrement>
</ColorFieldRoot>
</template>TIP
All components are completely unstyled — the classes above are just an example using Tailwind CSS. Use any styling approach you prefer.
Multiple channels
To build a full channel editor, loop over the channels in a color space using the colorSpaces helper from @urcolor/core:
<script setup lang="ts">
import { computed } from "vue";
import { colorSpaces } from "@urcolor/core";
import {
useColor,
ColorFieldRoot,
ColorFieldInput,
ColorFieldIncrement,
ColorFieldDecrement,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
const channels = computed(() => colorSpaces["hsl"]?.channels ?? []);
</script>
<template>
<div class="flex gap-2">
<div v-for="ch in channels" :key="ch.key" class="flex flex-col gap-1">
<label class="text-xs font-semibold">{{ ch.label }}</label>
<ColorFieldRoot v-model="color" color-space="hsl" :channel="ch.key">
<ColorFieldDecrement>−</ColorFieldDecrement>
<ColorFieldInput />
<ColorFieldIncrement>+</ColorFieldIncrement>
</ColorFieldRoot>
</div>
</div>
</template>Each ColorFieldRoot shares the same v-model — updating one channel automatically keeps the others in sync.
Hex format
Set channel to "hex" and format to "hex" for a hex color input:
<template>
<ColorFieldRoot
v-model="color"
color-space="hsl"
channel="hex"
format="hex"
>
<ColorFieldInput />
</ColorFieldRoot>
</template>Listening to changes
Use @update:model-value for real-time updates and @value-commit for the final value (on blur or Enter):
<script setup lang="ts">
// ...
const onColorChange = (color: Color) => {
console.log("changing", color.toString("css"));
};
const onColorCommit = (color: Color) => {
console.log("committed", color.toString("css"));
};
</script>
<template>
<ColorFieldRoot
v-model="color"
color-space="hsl"
channel="h"
@update:model-value="onColorChange"
@value-commit="onColorCommit"
>
<!-- ... -->
</ColorFieldRoot>
</template>