Build Color Picker (Square in Ring)
Let's combine a color ring and color area into an HSV color picker with a square gradient inside a hue ring.
Here's what we'll end up with:
Click to view the full code
<script setup lang="ts">
import "internationalized-color/css";
import {
useColor,
ColorRingRoot,
ColorRingTrack,
ColorRingGradient,
ColorRingThumb,
ColorAreaRoot,
ColorAreaTrack,
ColorAreaGradient,
ColorAreaThumb,
ColorAreaThumbX,
ColorAreaThumbY,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<div class="relative size-64">
<ColorRingRoot
v-model="color"
color-space="hsv"
channel="h"
:inner-radius="0.84"
as="div"
class="absolute inset-0"
style="container-type: inline-size"
>
<ColorRingTrack
as="div"
class="relative block size-full"
>
<ColorRingGradient
as="div"
class="absolute inset-0 block"
:channel-overrides="{ s: 1, v: 1, alpha: 1 }"
/>
<ColorRingThumb
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="Hue"
/>
</ColorRingTrack>
</ColorRingRoot>
<ColorAreaRoot
v-model="color"
color-space="hsv"
channel-x="s"
channel-y="v"
:inverted-y="true"
as="div"
class="absolute inset-[20.3%]"
>
<ColorAreaTrack
as="div"
class="relative size-full cursor-crosshair touch-none overflow-clip rounded-sm"
>
<ColorAreaGradient
as="div"
class="absolute inset-0"
/>
<ColorAreaThumb
as="div"
class="
absolute size-5 transform-(--reka-slider-area-thumb-transform)
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)]
"
>
<ColorAreaThumbX class="outline-none" />
<ColorAreaThumbY class="outline-none" />
</ColorAreaThumb>
</ColorAreaTrack>
</ColorAreaRoot>
</div>
</template>Step 1: Set up shared state
Both components will share the same color ref. Start with a single reactive color value.
<script setup lang="ts">
import { useColor } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>Step 2: Add the outer hue ring
The ColorRing handles hue selection. We use a relative container to position the ring and square together.
<script setup lang="ts">
import {
useColor,
ColorRingRoot,
ColorRingTrack,
ColorRingGradient,
ColorRingThumb,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<div class="relative size-64">
<ColorRingRoot
v-model="color"
color-space="hsv"
channel="h"
:inner-radius="0.84"
class="absolute inset-0"
style="container-type: inline-size"
>
<ColorRingTrack class="relative block size-full">
<ColorRingGradient
class="absolute inset-0 block"
:channel-overrides="{ s: 1, v: 1, alpha: 1 }"
/>
<ColorRingThumb
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="Hue"
/>
</ColorRingTrack>
</ColorRingRoot>
</div>
</template>The outer div acts as the layout container. The ring is absolutely positioned to fill it.
Step 3: Add the inner color area
Place a ColorArea inside the ring to control saturation and value. Use absolute positioning with inset-[20.3%] to inscribe the square perfectly inside the ring's inner circle.
<script setup lang="ts">
import {
useColor,
ColorRingRoot,
ColorRingTrack,
ColorRingGradient,
ColorRingThumb,
ColorAreaRoot,
ColorAreaTrack,
ColorAreaGradient,
ColorAreaThumb,
ColorAreaThumbX,
ColorAreaThumbY,
} from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<div class="relative size-64">
<ColorRingRoot
v-model="color"
color-space="hsv"
channel="h"
:inner-radius="0.84"
class="absolute inset-0"
style="container-type: inline-size"
>
<ColorRingTrack class="relative block size-full">
<ColorRingGradient
class="absolute inset-0 block"
:channel-overrides="{ s: 1, v: 1, alpha: 1 }"
/>
<ColorRingThumb
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="Hue"
/>
</ColorRingTrack>
</ColorRingRoot>
<ColorAreaRoot
v-model="color"
color-space="hsv"
channel-x="s"
channel-y="v"
:inverted-y="true"
class="absolute inset-[20.3%]"
>
<ColorAreaTrack class="relative size-full cursor-crosshair touch-none overflow-clip rounded-sm">
<ColorAreaGradient class="absolute inset-0" />
<ColorAreaThumb
class="
absolute size-5 transform-(--reka-slider-area-thumb-transform)
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)]
"
>
<ColorAreaThumbX />
<ColorAreaThumbY />
</ColorAreaThumb>
</ColorAreaTrack>
</ColorAreaRoot>
</div>
</template>The key is inset-[20.3%] — this inscribes the square perfectly inside the ring's inner circle (50% × (1 − 0.84/√2) ≈ 20.3%). The :inverted-y="true" prop flips the Y axis so value increases upward. Both components share the same v-model="color", so dragging the hue ring updates the area's gradient, and dragging the area updates the color while keeping the hue ring in sync.
TIP
All components are completely unstyled — the classes above are just an example using Tailwind CSS. To compute the inset for any inner-radius, use 50% × (1 − innerRadius / √2).