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
import "internationalized-color/css";
import { colorSpaces } from "@urcolor/core";
import { ColorField, useColor } from "@urcolor/react";
export default function ColorFieldGuide() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
const channels = colorSpaces["hsl"]?.channels ?? [];
return (
<div className="flex flex-1 flex-wrap gap-2">
{channels.map((ch) => (
<div key={ch.key} className="flex min-w-[80px] flex-1 flex-col gap-1">
<label
htmlFor={`guide-field-${ch.key}`}
className="text-xs font-semibold text-[var(--vp-c-text-2)]"
>
{ch.label}
</label>
<ColorField.Root
value={color}
onValueChange={setColor}
colorSpace="hsl"
channel={ch.key}
className="
flex items-center overflow-hidden rounded-md border
border-[var(--vp-c-divider)] bg-[var(--vp-c-bg)]
"
>
<ColorField.Decrement
className="
flex size-8 shrink-0 cursor-pointer items-center justify-center
border-none bg-transparent text-lg leading-none text-[var(--vp-c-text-2)]
select-none
hover:not-disabled:bg-[var(--vp-c-bg-soft)]
hover:not-disabled:text-[var(--vp-c-text-1)]
disabled:cursor-default disabled:opacity-30
"
>
−
</ColorField.Decrement>
<ColorField.Input
id={`guide-field-${ch.key}`}
className="
w-0 min-w-0 flex-1 border-none bg-transparent px-0.5 py-1
text-center font-mono text-[13px] text-[var(--vp-c-text-1)] outline-none
"
/>
<ColorField.Increment
className="
flex size-8 shrink-0 cursor-pointer items-center justify-center
border-none bg-transparent text-lg leading-none text-[var(--vp-c-text-2)]
select-none
hover:not-disabled:bg-[var(--vp-c-bg-soft)]
hover:not-disabled:text-[var(--vp-c-text-1)]
disabled:cursor-default disabled:opacity-30
"
>
+
</ColorField.Increment>
</ColorField.Root>
</div>
))}
</div>
);
}Step 1: Set up state
Start by importing the color hook and creating color state.
import { useColor } from "@urcolor/react";
function MyField() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
}Step 2: Add the root
ColorField.Root manages the state for a single channel input. Tell it which color space and channel to control.
import { useColor, ColorField } from "@urcolor/react";
function MyField() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
return (
<ColorField.Root
value={color}
onValueChange={setColor}
colorSpace="hsl"
channel="h"
>
{/* children go here */}
</ColorField.Root>
);
}Step 3: Add the input
ColorField.Input renders the numeric input. It automatically formats the value based on the channel.
import { useColor, ColorField } from "@urcolor/react";
function MyField() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
return (
<ColorField.Root
value={color}
onValueChange={setColor}
colorSpace="hsl"
channel="h"
className="
flex items-center overflow-hidden rounded-md border
border-[var(--vp-c-divider)] bg-[var(--vp-c-bg)]
"
>
<ColorField.Input
className="
w-full border-none bg-transparent px-2 py-1
text-center font-mono text-sm outline-none
"
/>
</ColorField.Root>
);
}Step 4: Add increment and decrement buttons
ColorField.Increment and ColorField.Decrement provide stepper buttons.
import { useColor, ColorField } from "@urcolor/react";
function MyField() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
return (
<ColorField.Root
value={color}
onValueChange={setColor}
colorSpace="hsl"
channel="h"
className="
flex items-center overflow-hidden rounded-md border
border-[var(--vp-c-divider)] bg-[var(--vp-c-bg)]
"
>
<ColorField.Decrement className="flex size-8 items-center justify-center">
−
</ColorField.Decrement>
<ColorField.Input
className="
w-0 min-w-0 flex-1 border-none bg-transparent px-0.5 py-1
text-center font-mono text-[13px] outline-none
"
/>
<ColorField.Increment className="flex size-8 items-center justify-center">
+
</ColorField.Increment>
</ColorField.Root>
);
}TIP
All components are completely unstyled — the classes above are just an example using Tailwind CSS. Use any styling approach you prefer.
Multiple channels
Loop over the channels in a color space using the colorSpaces helper from @urcolor/core:
import { colorSpaces } from "@urcolor/core";
import { useColor, ColorField } from "@urcolor/react";
function MyFields() {
const { color, setColor } = useColor("hsl(210, 80%, 50%)");
const channels = colorSpaces["hsl"]?.channels ?? [];
return (
<div className="flex gap-2">
{channels.map((ch) => (
<div key={ch.key} className="flex flex-col gap-1">
<label className="text-xs font-semibold">{ch.label}</label>
<ColorField.Root value={color} onValueChange={setColor} colorSpace="hsl" channel={ch.key}>
<ColorField.Decrement>−</ColorField.Decrement>
<ColorField.Input />
<ColorField.Increment>+</ColorField.Increment>
</ColorField.Root>
</div>
))}
</div>
);
}Each ColorField.Root shares the same state — updating one channel automatically keeps the others in sync.
Hex format
Set channel to "hex" and format to "hex" for a hex color input:
<ColorField.Root
value={color}
onValueChange={setColor}
colorSpace="hsl"
channel="hex"
format="hex"
>
<ColorField.Input />
</ColorField.Root>Listening to changes
Use onValueChange for real-time updates and onValueCommit for the final value (on blur or Enter):
import { Color } from "internationalized-color";
const onColorChange = (color: Color) => {
console.log("changing", color.toString("css"));
};
const onColorCommit = (color: Color) => {
console.log("committed", color.toString("css"));
};
<ColorField.Root
value={color}
onValueChange={onColorChange}
onValueCommit={onColorCommit}
colorSpace="hsl"
channel="h"
>
{/* ... */}
</ColorField.Root>