How to Build Color Swatches
A swatch renders one color as a filled element, static or selectable.
Here's what we'll end up with:
Click to view the full code
<script setup lang="ts">
import { shallowRef } from "vue";
import { Color } from "@urcolor/core";
import { ColorSwatchRoot } from "@urcolor/vue";
import { Check } from "lucide-vue-next";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
const selected = shallowRef(colors[0]);
</script>
<template>
<div class="flex items-center gap-3">
<ColorSwatchRoot
v-for="(color, i) in colors"
:key="i"
:model-value="color"
alpha
class="flex size-10 cursor-pointer items-center justify-center rounded-lg"
@click="selected = color"
>
<Check
class="
size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]
transition-opacity duration-150
"
:class="selected === color ? 'opacity-100' : 'opacity-0'"
/>
</ColorSwatchRoot>
</div>
</template>import { useState } from "react";
import { Color } from "@urcolor/core";
import { ColorSwatch } from "@urcolor/react";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
export default function ColorSwatchGuide() {
const [selected, setSelected] = useState(0);
return (
<div className="flex items-center gap-3">
{colors.map((color, i) => (
<ColorSwatch
key={i}
value={color}
alpha
className="flex size-10 cursor-pointer items-center justify-center rounded-lg"
onClick={() => setSelected(i)}
>
<svg
className={`size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150 ${selected === i ? "opacity-100" : "opacity-0"}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</ColorSwatch>
))}
</div>
);
}<script lang="ts">
import { Color } from "@urcolor/core";
import { ColorSwatch } from "@urcolor/svelte";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
let selected = $state(0);
</script>
<div class="flex items-center gap-3">
{#each colors as color, i (i)}
<ColorSwatch
value={color}
alpha
class="flex size-10 cursor-pointer items-center justify-center rounded-lg"
onclick={() => (selected = i)}
>
<svg
class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150 {selected
=== i
? 'opacity-100'
: 'opacity-0'}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</ColorSwatch>
{/each}
</div>import { Component, signal } from "@angular/core";
import { Color } from "@urcolor/core";
import { COLOR_SWATCH_DIRECTIVES } from "@urcolor/angular";
@Component({
selector: "color-swatch-guide",
imports: [...COLOR_SWATCH_DIRECTIVES],
template: `
<div class="flex items-center gap-3">
@for (color of colors; track $index) {
<div
urcColorSwatch
[value]="color"
alpha
class="flex size-10 cursor-pointer items-center justify-center rounded-lg"
(click)="selected.set($index)"
>
<svg
class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150"
[style.opacity]="selected() === $index ? 1 : 0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
}
</div>
`,
})
export class ColorSwatchGuide {
protected readonly colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
protected readonly selected = signal(0);
}The parts, and how they nest:
Step 1: Set up state
Import the color model and define the colors.
<script setup lang="ts">
import { useColor } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>import { useColor } from "@urcolor/react";
function MySwatch() {
const { color } = useColor("hsl(210, 80%, 50%)");
}<script lang="ts">
import { useColor } from "@urcolor/svelte";
const colorState = useColor("hsl(210, 80%, 50%)");
</script>import { Component, signal } from "@angular/core";
import { Color } from "@urcolor/core";
@Component({
selector: "my-swatch",
template: ``,
})
export class MySwatch {
protected readonly color = signal<Color>(Color.parse("hsl(210, 80%, 50%)")!);
}useColor() creates color state from any CSS color string. Vue returns a { color } shallow ref and React returns { color, setColor }. Svelte returns a rune-backed object whose color, hex and alpha are getters, so keep the object and read colorState.color rather than destructuring it, or reactivity is lost. Angular has no hook: a plain signal<Color>() is the state, and the swatch reads it with [value]="color()".
Step 2: Render a swatch
The swatch root renders one color as a filled element. Vue passes it as model-value, React and Svelte as value, Angular as [value].
<script setup lang="ts">
import { useColor, ColorSwatchRoot } from "@urcolor/vue";
const { color } = useColor("hsl(210, 80%, 50%)");
</script>
<template>
<ColorSwatchRoot
:model-value="color"
class="size-10 rounded-lg"
/>
</template>import { useColor, ColorSwatch } from "@urcolor/react";
function MySwatch() {
const { color } = useColor("hsl(210, 80%, 50%)");
return (
<ColorSwatch.Root
value={color}
className="size-10 rounded-lg"
/>
);
}<script lang="ts">
import { ColorSwatch, useColor } from "@urcolor/svelte";
const colorState = useColor("hsl(210, 80%, 50%)");
</script>
<ColorSwatch
value={colorState.color}
class="size-10 rounded-lg"
/>import { Component, signal } from "@angular/core";
import { Color } from "@urcolor/core";
import { COLOR_SWATCH_DIRECTIVES } from "@urcolor/angular";
@Component({
selector: "my-swatch",
imports: [...COLOR_SWATCH_DIRECTIVES],
template: `
<div
urcColorSwatch
[value]="color()"
class="size-10 rounded-lg"
></div>
`,
})
export class MySwatch {
protected readonly color = signal<Color>(Color.parse("hsl(210, 80%, 50%)")!);
}The swatch paints the color as a background and nothing else. Sizing, border radius and the rest are yours.
Svelte and Angular ship the swatch as a single part rather than a Root namespace member: ColorSwatch is the component itself, and [urcColorSwatch] is an attribute directive for whatever element you want: a <div> for a static sample, a <button> when it should be pressable. The value prop is display-only in all four frameworks; the two-way binding on a swatch is pressed, which Svelte exposes as $bindable (bind:pressed) and Angular as a model() ([(pressed)]).
Angular ships each family as a COLOR_*_DIRECTIVES array, so one entry in imports brings in the whole set.
TIP
The components ship unstyled. The classes above are one example, written with Tailwind CSS; any styling approach works.
Alpha transparency
The alpha prop puts a checkerboard behind a semi-transparent color:
<template>
<ColorSwatchRoot
:model-value="color"
alpha
class="size-10 rounded-lg"
/>
</template><ColorSwatch.Root
value={color}
alpha
className="size-10 rounded-lg"
/><ColorSwatch
value={colorState.color}
alpha
class="size-10 rounded-lg"
/><div
urcColorSwatch
[value]="color()"
alpha
class="size-10 rounded-lg"
></div>Without alpha the color renders fully opaque, whatever its alpha channel says.
Checkerboard size
The checkerboard cell size, in pixels:
<template>
<ColorSwatchRoot
:model-value="color"
alpha
:checker-size="8"
class="size-10 rounded-lg"
/>
</template><ColorSwatch.Root
value={color}
alpha
checkerSize={8}
className="size-10 rounded-lg"
/><ColorSwatch
value={colorState.color}
alpha
checkerSize={8}
class="size-10 rounded-lg"
/><div
urcColorSwatch
[value]="color()"
alpha
checkerSize="8"
class="size-10 rounded-lg"
></div>Multiple swatches
A palette is a loop over an array of colors:
<script setup lang="ts">
import { shallowRef } from "vue";
import { Color } from "@urcolor/core";
import { ColorSwatchRoot } from "@urcolor/vue";
import { Check } from "lucide-vue-next";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
const selected = shallowRef(colors[0]);
</script>
<template>
<div class="flex items-center gap-3">
<ColorSwatchRoot
v-for="(color, i) in colors"
:key="i"
:model-value="color"
alpha
class="
size-10 cursor-pointer rounded-lg
flex items-center justify-center
"
@click="selected = color"
>
<Check
class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150"
:class="selected === color ? 'opacity-100' : 'opacity-0'"
/>
</ColorSwatchRoot>
</div>
</template>import { useState } from "react";
import { Color } from "@urcolor/core";
import { ColorSwatch } from "@urcolor/react";
import { Check } from "lucide-react";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
function MyPalette() {
const [selected, setSelected] = useState(0);
return (
<div className="flex items-center gap-3">
{colors.map((color, i) => (
<ColorSwatch.Root
key={i}
value={color}
alpha
className="size-10 cursor-pointer rounded-lg flex items-center justify-center"
onClick={() => setSelected(i)}
>
<Check
className="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150"
style={{ opacity: selected === i ? 1 : 0 }}
/>
</ColorSwatch.Root>
))}
</div>
);
}<script lang="ts">
import { Color } from "@urcolor/core";
import { ColorSwatch } from "@urcolor/svelte";
const colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
let selected = $state(0);
</script>
<div class="flex items-center gap-3">
{#each colors as color, i (i)}
<ColorSwatch
value={color}
alpha
class="flex size-10 cursor-pointer items-center justify-center rounded-lg"
onclick={() => (selected = i)}
>
<svg
class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150 {selected === i ? 'opacity-100' : 'opacity-0'}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</ColorSwatch>
{/each}
</div>import { Component, signal } from "@angular/core";
import { Color } from "@urcolor/core";
import { COLOR_SWATCH_DIRECTIVES } from "@urcolor/angular";
@Component({
selector: "my-palette",
imports: [...COLOR_SWATCH_DIRECTIVES],
template: `
<div class="flex items-center gap-3">
@for (color of colors; track $index) {
<div
urcColorSwatch
[value]="color"
alpha
class="flex size-10 cursor-pointer items-center justify-center rounded-lg"
(click)="selected.set($index)"
>
<svg
class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150"
[style.opacity]="selected() === $index ? 1 : 0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
}
</div>
`,
})
export class MyPalette {
protected readonly colors = [
Color.parse("hsl(210, 80%, 50%)")!,
Color.parse("hsl(350, 90%, 60%)")!,
Color.parse("hsl(120, 60%, 45%)")!,
Color.parse("hsla(45, 100%, 55%, 0.5)")!,
];
protected readonly selected = signal(0);
}Svelte reaches for {#each} and Angular for @for, and both keep the click handler on the swatch itself. Svelte's onclick lands on the rendered element through the rest props, and Angular's (click) is a native listener on the element you own. If you would rather not track selection by hand, bind pressed instead (bind:pressed in Svelte, [(pressed)] in Angular) and the swatch upgrades itself to a toggle button with aria-pressed, Enter/Space activation, and a data-pressed attribute to style against.
String colors
A plain CSS color string works in place of a Color object:
<template>
<ColorSwatchRoot
model-value="oklch(0.6 0.15 210)"
class="size-10 rounded-lg"
/>
</template><ColorSwatch.Root
value="oklch(0.6 0.15 210)"
className="size-10 rounded-lg"
/><ColorSwatch
value="oklch(0.6 0.15 210)"
class="size-10 rounded-lg"
/><div
urcColorSwatch
value="oklch(0.6 0.15 210)"
class="size-10 rounded-lg"
></div>