Skip to content

Build a Color Swatch Group

Let's build a keyboard-navigable color swatch group for selecting colors from a palette.

Here's what we'll end up with:

Selected: hsl(210, 80%, 50%)

Click to view the full code
vue
<script setup lang="ts">
import { ref } from "vue";
import "internationalized-color/css";
import { ColorSwatchGroupRoot, ColorSwatchGroupItem } from "@urcolor/vue";
import { Check } from "lucide-vue-next";

const colors = [
  "hsl(210, 80%, 50%)",
  "hsl(350, 90%, 60%)",
  "hsl(120, 60%, 45%)",
  "hsl(45, 100%, 55%)",
  "hsl(280, 70%, 55%)",
  "hsl(15, 85%, 55%)",
];

const selected = ref<string[]>([colors[0]!]);
</script>

<template>
  <div class="flex flex-col gap-4">
    <ColorSwatchGroupRoot
      v-model="selected"
      type="single"
      class="flex items-center gap-2"
    >
      <ColorSwatchGroupItem
        v-for="color in colors"
        :key="color"
        :value="color"
        class="
          flex size-10 cursor-pointer items-center justify-center rounded-lg
          outline-none
        "
      >
        <Check
          class="
            size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]
            transition-opacity duration-150
          "
          :class="selected.includes(color) ? 'opacity-100' : 'opacity-0'"
        />
      </ColorSwatchGroupItem>
    </ColorSwatchGroupRoot>
    <p class="text-sm text-(--vp-c-text-2)">
      Selected: <code>{{ selected[0] ?? 'none' }}</code>
    </p>
  </div>
</template>

Step 1: Set up state

Define your color palette and a reactive selection state.

vue
<script setup lang="ts">
import { ref } from "vue";  

const colors = [  
  "hsl(210, 80%, 50%)",  
  "hsl(350, 90%, 60%)",  
  "hsl(120, 60%, 45%)",  
  "hsl(45, 100%, 55%)",  
];  

const selected = ref<string[]>([colors[0]!]);  
</script>

The selection is always an array of strings — even for single selection mode.

Step 2: Add the group root

ColorSwatchGroupRoot manages selection state and keyboard navigation (arrow keys, roving focus).

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

const colors = [
  "hsl(210, 80%, 50%)",
  "hsl(350, 90%, 60%)",
  "hsl(120, 60%, 45%)",
  "hsl(45, 100%, 55%)",
];

const selected = ref<string[]>([colors[0]!]);
</script>

<template>
  <ColorSwatchGroupRoot
    v-model="selected"
    type="single"
    class="flex items-center gap-2"
  >
    <!-- items go here -->
  </ColorSwatchGroupRoot>
</template>
  • type="single" — only one color can be selected at a time
  • type="multiple" — multiple colors can be selected

Step 3: Add swatch items

ColorSwatchGroupItem renders each selectable color. The value prop is both the selection value and the displayed color.

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  ColorSwatchGroupRoot,
  ColorSwatchGroupItem, 
} from "@urcolor/vue";
import { Check } from "lucide-vue-next"; 

const colors = [
  "hsl(210, 80%, 50%)",
  "hsl(350, 90%, 60%)",
  "hsl(120, 60%, 45%)",
  "hsl(45, 100%, 55%)",
];

const selected = ref<string[]>([colors[0]!]);
</script>

<template>
  <ColorSwatchGroupRoot
    v-model="selected"
    type="single"
    class="flex items-center gap-2"
  >
    <ColorSwatchGroupItem
      v-for="color in colors"
      :key="color"
      :value="color"
      class="
        size-10 cursor-pointer rounded-lg
        flex items-center justify-center
        outline-none
      "
    >
      <Check
        class="size-5 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] transition-opacity duration-150"
        :class="selected.includes(color) ? 'opacity-100' : 'opacity-0'"
      />
    </ColorSwatchGroupItem>
  </ColorSwatchGroupRoot>
</template>

Each item automatically renders as a <button> with the appropriate ARIA role (radio for single, checkbox for multiple). The data-state attribute is "on" when selected and "off" otherwise — use it for styling.

TIP

All components are completely unstyled — the classes above are just an example using Tailwind CSS. Use any styling approach you prefer.

Multiple selection

Change type to "multiple" to allow selecting more than one color:

vue
<template>
  <ColorSwatchGroupRoot
    v-model="selected"
    type="multiple"
    class="flex items-center gap-2"
  >
    <ColorSwatchGroupItem
      v-for="color in colors"
      :key="color"
      :value="color"
      class="..."
    />
  </ColorSwatchGroupRoot>
</template>

Keyboard navigation

The group supports full keyboard navigation out of the box:

  • Arrow keys — move focus between swatches
  • Space / Enter — toggle selection
  • Home / End — jump to first or last swatch

You can configure navigation behavior with these props on the root:

vue
<template>
  <ColorSwatchGroupRoot
    v-model="selected"
    type="single"
    :loop="true"
    :roving-focus="true"
    orientation="horizontal"
  >
    <!-- ... -->
  </ColorSwatchGroupRoot>
</template>
  • loop — whether arrow keys wrap around (default: true)
  • roving-focus — whether to use roving tabindex (default: true)
  • orientation"horizontal" or "vertical" for arrow key direction

Disabled items

Disable individual swatches or the entire group:

vue
<template>
  <ColorSwatchGroupRoot
    :disabled="true"
    v-model="selected"
    type="single"
  >
    <ColorSwatchGroupItem
      :value="color"
      :disabled="true"
    />
  </ColorSwatchGroupRoot>
</template>

Listening to changes

Use @update:model-value to react to selection changes:

vue
<script setup lang="ts">
// ...
const onSelectionChange = (value: string[]) => {
  console.log("selected:", value);
};
</script>

<template>
  <ColorSwatchGroupRoot
    v-model="selected"
    @update:model-value="onSelectionChange"
    type="single"
  >
    <!-- ... -->
  </ColorSwatchGroupRoot>
</template>