Skip to content

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
vue
<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>
tsx
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>
  );
}
svelte
<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>
ts
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.

vue
<script setup lang="ts">
import { useColor } from "@urcolor/vue";  

const { color } = useColor("hsl(210, 80%, 50%)");  
</script>
tsx
import { useColor } from "@urcolor/react"; 

function MySwatch() {
  const { color } = useColor("hsl(210, 80%, 50%)"); 
}
svelte
<script lang="ts">
  import { useColor } from "@urcolor/svelte"; 

  const colorState = useColor("hsl(210, 80%, 50%)"); 
</script>
ts
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].

vue
<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>
tsx
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"
    />
  );
}
svelte
<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"
/>
ts
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:

vue
<template>
  <ColorSwatchRoot
    :model-value="color"
    alpha
    class="size-10 rounded-lg"
  />
</template>
tsx
<ColorSwatch.Root
  value={color}
  alpha
  className="size-10 rounded-lg"
/>
svelte
<ColorSwatch
  value={colorState.color}
  alpha
  class="size-10 rounded-lg"
/>
html
<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:

vue
<template>
  <ColorSwatchRoot
    :model-value="color"
    alpha
    :checker-size="8"
    class="size-10 rounded-lg"
  />
</template>
tsx
<ColorSwatch.Root
  value={color}
  alpha
  checkerSize={8}
  className="size-10 rounded-lg"
/>
svelte
<ColorSwatch
  value={colorState.color}
  alpha
  checkerSize={8}
  class="size-10 rounded-lg"
/>
html
<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:

vue
<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>
tsx
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>
  );
}
svelte
<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>
ts
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:

vue
<template>
  <ColorSwatchRoot
    model-value="oklch(0.6 0.15 210)"
    class="size-10 rounded-lg"
  />
</template>
tsx
<ColorSwatch.Root
  value="oklch(0.6 0.15 210)"
  className="size-10 rounded-lg"
/>
svelte
<ColorSwatch
  value="oklch(0.6 0.15 210)"
  class="size-10 rounded-lg"
/>
html
<div
  urcColorSwatch
  value="oklch(0.6 0.15 210)"
  class="size-10 rounded-lg"
></div>