Build Color Wheel
Let's build a 2D color wheel step by step.
Here's what we'll end up with:
Click to view the full code
<script setup lang="ts">
import "internationalized-color/css";
import {
useColor,
ColorWheelRoot,
ColorWheelGradient,
ColorWheelThumb,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
channel-angle="h"
channel-radius="s"
as="div"
class="relative block size-64 overflow-hidden rounded-full"
style="container-type: inline-size"
>
<ColorWheelGradient
as="div"
class="absolute inset-0 block"
/>
<ColorWheelThumb
class="
size-4 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)]
focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
"
aria-label="Color"
/>
</ColorWheelRoot>
</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>useColor() creates a reactive color ref from any CSS color string. It returns a { color } object where color is a shallow ref holding the parsed Color instance.
Step 2: Add the root
ColorWheelRoot manages all the state and interactions. Tell it which color space and channels to map to the angle and radius.
<script setup lang="ts">
import { useColor, ColorWheelRoot } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
channel-angle="h"
channel-radius="s"
>
<!-- children go here -->
</ColorWheelRoot>
</template>color-space— the color space to work in (hsl,oklch, etc.)channel-angle— the channel mapped to the angular axis (rotation)channel-radius— the channel mapped to the radial axis (distance from center)
Step 3: Add the gradient
ColorWheelGradient renders the 2D circular gradient on a canvas.
<script setup lang="ts">
import {
useColor,
ColorWheelRoot,
ColorWheelGradient,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
channel-angle="h"
channel-radius="s"
class="relative block size-64 overflow-hidden rounded-full"
style="container-type: inline-size"
>
<ColorWheelGradient class="absolute inset-0 block" />
</ColorWheelRoot>
</template>The root needs overflow-hidden rounded-full to clip the gradient to a circle, and container-type: inline-size so the component can calculate dimensions correctly.
Step 4: Add the thumb
ColorWheelThumb is the draggable handle. It's positioned automatically within the wheel.
<script setup lang="ts">
import {
useColor,
ColorWheelRoot,
ColorWheelGradient,
ColorWheelThumb,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
channel-angle="h"
channel-radius="s"
class="relative block size-64 overflow-hidden rounded-full"
style="container-type: inline-size"
>
<ColorWheelGradient class="absolute inset-0 block" />
<ColorWheelThumb
class="
size-4 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)]
focus-visible:shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_0_3px_rgba(66,153,225,0.6)]
"
aria-label="Color"
/>
</ColorWheelRoot>
</template>TIP
All components are completely unstyled — the classes above are just an example using Tailwind CSS. Use any styling approach you prefer.
Start angle offset
Use start-angle to rotate where the wheel gradient begins (in degrees):
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
:start-angle="90"
channel-angle="h"
channel-radius="s"
>
<!-- ... -->
</ColorWheelRoot>
</template>Different color spaces
Switch the color space and channel mapping for different wheel behaviors. For example, OKLCh:
<script setup lang="ts">
import { useColor } from "@urcolor/vue";
const { color } = useColor("oklch(0.6, 0.15, 210)");
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="oklch"
channel-angle="hue"
channel-radius="chroma"
>
<!-- ... -->
</ColorWheelRoot>
</template>Listening to changes
Use @update:model-value for real-time updates (while dragging) and @value-commit for the final value (on release):
<script setup lang="ts">
// ...
const onColorChange = (color: Color) => {
console.log("dragging", color.toString("css"));
};
const onColorCommit = (color: Color) => {
console.log("committed", color.toString("css"));
};
</script>
<template>
<ColorWheelRoot
v-model="color"
color-space="hsl"
@update:model-value="onColorChange"
@value-commit="onColorCommit"
channel-angle="h"
channel-radius="s"
>
<!-- ... -->
</ColorWheelRoot>
</template>