---
name: swatch-group
title: Swatch Group
url: https://ui.frontic.com/components/ui/swatch-group.md
tier: ui
install: npx @frontic/ui add swatch-group
files:
  - components/ui/swatch-group/SwatchGroup.vue
  - components/ui/swatch-group/SwatchGroupItem.vue
  - components/ui/swatch-group/index.ts
registryDependencies:
  - swatch
  - use-field-control
dependencies:
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Swatch Group

A row or column of swatches for choosing one option, such as a colour, or several, as in a filter; joins a Field around it.

## Install

```sh
npx @frontic/ui add swatch-group
```

Built from: [Swatch](https://ui.frontic.com/components/ui/swatch.md), [Use Field Control](https://ui.frontic.com/composables/use-field-control.md)

Used by: [Filter](https://ui.frontic.com/components/ui/filter.md), [Product Card Basic](https://ui.frontic.com/components/sections/product-card-basic.md), [Product Card Featured](https://ui.frontic.com/components/sections/product-card-featured.md), [Product Card Gallery](https://ui.frontic.com/components/sections/product-card-gallery.md), [Product Card Horizontal](https://ui.frontic.com/components/sections/product-card-horizontal.md), [Product Card Hover](https://ui.frontic.com/components/sections/product-card-hover.md), [Product Card Minimal](https://ui.frontic.com/components/sections/product-card-minimal.md), [Product Options](https://ui.frontic.com/components/sections/product-options.md)

npm packages: `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { SwatchGroup, SwatchGroupItem } from '@/components/ui/swatch-group'

const selected = ref('aubergine')
</script>

<template>
  <SwatchGroup v-model="selected" type="single" aria-label="Colour">
    <SwatchGroupItem value="aubergine" hex="#3d3347" label="Aubergine" />
    <SwatchGroupItem value="dusty-rose" hex="#b98a87" label="Dusty rose" />
    <SwatchGroupItem value="salmon" hex="#dc967b" label="Salmon" unavailable unavailable-label="Sold out" />
  </SwatchGroup>
</template>
```

## Anatomy

```vue
<SwatchGroup>
  <SwatchGroupItem />
</SwatchGroup>
```

## API reference

### SwatchGroup

Groups swatches so the user can choose one or several options.

`data-slot="swatch-group"` · `SwatchGroup.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rovingFocus` | `boolean` | `true` | When `false`, navigating through the items using arrow keys will be disabled. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the toggle group and all its items. |
| `orientation` | `'vertical' \| 'horizontal'` | `'horizontal'` | The orientation of the component, which determines how focus moves: `horizontal` for left/right arrows and `vertical` for up/down arrows. Lays the swatches out in a row, wrapping, or in a column with each label beside its swatch. |
| `dir` | `Direction`: `'ltr' \| 'rtl'` |  | The reading direction of the combobox when applicable.<br><br>If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. |
| `loop` | `boolean` | `true` | When `loop` and `rovingFocus` is `true`, keyboard navigation will loop from last item to first, and vice versa. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `name` | `string` |  | The name of the field. Submitted with its owning form as part of a name/value pair. |
| `required` | `boolean` | `false` | When `true`, indicates that the user must set the value before the owning form can be submitted. |
| `type` | `SingleOrMultipleType`: `'single' \| 'multiple'` |  | Determines whether a "single" or "multiple" items can be selected at a time.<br><br>This prop will overwrite the inferred type from `modelValue` and `defaultValue`. |
| `modelValue` | `AcceptableValue \| AcceptableValue[]`: `null \| string \| number \| bigint \| Record<string, any> \| AcceptableValue[]` |  | The controlled value of the active item(s).<br><br>Use this when you need to control the state of the items. Can be binded with `v-model` |
| `defaultValue` | `AcceptableValue \| AcceptableValue[]`: `null \| string \| number \| bigint \| Record<string, any> \| AcceptableValue[]` |  | The default active value of the item(s).<br><br>Use when you do not need to control the state of the item(s). |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `SwatchSize`: `'md' \| 'xs' \| 'sm' \| 'lg'` |  | The size of every swatch in the group. |
| `shape` | `SwatchShape`: `'default' \| 'pill'` |  | The corner shape of every swatch: rounded or a full circle. |
| `labelClass` | `ClassValue`: `null \| string \| false \| Record<string, any> \| ClassValue[]` |  | Classes added to every swatch's label; an item's own `labelClass` wins. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `payload: AcceptableValue \| AcceptableValue[]` | Event handler called when the value changes. |

### SwatchGroupItem

Lets the user select one option within its swatch group.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `AcceptableValue`: `null \| string \| number \| bigint \| Record<string, any>` |  | A string value for the toggle group item. All items within a toggle group should use a unique value. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the toggle. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `SwatchSize`: `'md' \| 'xs' \| 'sm' \| 'lg'` |  | The swatch size when the group sets none. |
| `shape` | `SwatchShape`: `'default' \| 'pill'` |  | The corner shape when the group sets none: rounded or a full circle. |
| `hex` | `string` |  | The colour the swatch shows, as a hex code such as `#1f2937`. |
| `label` | `string` |  | Shown with the swatch, e.g. the colour's name. |
| `labelClass` | `ClassValue`: `null \| string \| false \| Record<string, any> \| ClassValue[]` |  | Classes added to the label; the group's `labelClass` applies when left out. |
| `unavailable` | `boolean` | `false` | Strikes the swatch through, e.g. for a sold-out colour; it stays selectable. |
| `unavailableLabel` | `string` |  | Read to screen readers after the swatch's name when it is unavailable. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `label` |  | Replaces the label shown below or beside the swatch. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-unavailable` |  | Present when the date cannot be picked. |

## Exports

```ts
import { type SwatchGroupOrientation } from '@/components/ui/swatch-group'
```

- `SwatchGroupOrientation`: `'horizontal' \| 'vertical'`

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`swatch-group`](https://ui.frontic.com/DESIGN.md#swatch-group) |

States with a look of their own: disabled.

## Source

`components/ui/swatch-group/SwatchGroup.vue`

```vue
<script lang="ts">
/** Groups swatches so the user can choose one or several options. */
export default {}
</script>

<script setup lang="ts">
import type { ToggleGroupRootEmits, ToggleGroupRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { SwatchShape, SwatchSize } from '@library/components/ui/swatch'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { ToggleGroupRoot, useForwardPropsEmits } from 'reka-ui'
import { provide, reactive, toRef, useTemplateRef } from 'vue'
import { type SwatchGroupOrientation, SWATCH_GROUP_INJECTION_KEY } from '.'
import { pickedItem, useFieldControl } from '@library/composables/use-field-control'

const swatchGroup = cva('fui-swatch-group', {
  variants: { orientation: { horizontal: 'fui-swatch-group-horizontal', vertical: 'fui-swatch-group-vertical' } },
  defaultVariants: { orientation: 'horizontal' },
})

const props = withDefaults(
  defineProps<
    ToggleGroupRootProps & {
      class?: HTMLAttributes['class']
      /** The size of every swatch in the group. */
      size?: SwatchSize
      /** The corner shape of every swatch: rounded or a full circle. */
      shape?: SwatchShape
      /** Lays the swatches out in a row, wrapping, or in a column with each label beside its swatch. */
      orientation?: SwatchGroupOrientation
      /** Classes added to every swatch's label; an item's own `labelClass` wins. */
      labelClass?: HTMLAttributes['class']
    }
  >(),
  {
    orientation: 'horizontal',
  },
)
const emits = defineEmits<ToggleGroupRootEmits>()

provide(
  SWATCH_GROUP_INJECTION_KEY,
  reactive({
    size: toRef(props, 'size'),
    shape: toRef(props, 'shape'),
    orientation: toRef(props, 'orientation'),
    labelClass: toRef(props, 'labelClass'),
  }),
)

const delegatedProps = reactiveOmit(props, 'class', 'size', 'shape', 'orientation', 'labelClass')
const forwarded = useForwardPropsEmits(delegatedProps, emits)

// Delete once Reka's ToggleGroup reads the Field itself.
const { joined, report } = useFieldControl({
  props,
  forwarded,
  root: useTemplateRef<{ $el: HTMLElement }>('root'),
  element: (root) => pickedItem(root, '[data-slot=swatch-group-item]', (item) => item.dataset.state === 'on'),
})
</script>

<template>
  <ToggleGroupRoot ref="root" data-slot="swatch-group" v-bind="joined" @update:model-value="report" :class="[swatchGroup({ orientation }), props.class]">
    <slot />
  </ToggleGroupRoot>
</template>
```

`components/ui/swatch-group/SwatchGroupItem.vue`

```vue
<script lang="ts">
/** Lets the user select one option within its swatch group. */
export default {}
</script>

<script setup lang="ts">
import type { ToggleGroupItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { Swatch, type SwatchShape, type SwatchSize } from '@library/components/ui/swatch'
import { SWATCH_GROUP_INJECTION_KEY } from '.'

const props = withDefaults(
  defineProps<
    ToggleGroupItemProps & {
      class?: HTMLAttributes['class']
      /** The swatch size when the group sets none. */
      size?: SwatchSize
      /** The corner shape when the group sets none: rounded or a full circle. */
      shape?: SwatchShape
      /** The colour the swatch shows, as a hex code such as `#1f2937`. */
      hex?: string
      /** Shown with the swatch, e.g. the colour's name. */
      label?: string
      /** Classes added to the label; the group's `labelClass` applies when left out. */
      labelClass?: HTMLAttributes['class']
      /** Strikes the swatch through, e.g. for a sold-out colour; it stays selectable. */
      unavailable?: boolean
      /** Read to screen readers after the swatch's name when it is unavailable. */
      unavailableLabel?: string
    }
  >(),
  {
    disabled: false,
    labelClass: undefined,
    unavailableLabel: undefined,
  },
)

const context = inject(SWATCH_GROUP_INJECTION_KEY, undefined)

defineSlots<{
  default?: () => unknown
  /** Replaces the label shown below or beside the swatch. */
  label?: () => unknown
}>()
</script>

<template>
  <Swatch
    :value="props.value"
    :disabled="props.disabled"
    :as="props.as"
    :as-child="props.asChild"
    :size="context?.size ?? props.size"
    :shape="context?.shape ?? props.shape"
    :hex="props.hex"
    :label="props.label"
    :label-class="props.labelClass ?? context?.labelClass"
    :unavailable="props.unavailable"
    :unavailable-label="props.unavailableLabel"
    :label-position="context?.orientation === 'vertical' ? 'end' : 'bottom'"
    :class="props.class"
  >
    <template v-if="$slots.default" #default>
      <slot />
    </template>
    <template v-if="$slots.label" #label>
      <slot name="label" />
    </template>
  </Swatch>
</template>
```

`components/ui/swatch-group/index.ts`

```ts
import type { HTMLAttributes, InjectionKey } from 'vue'
import type { SwatchShape, SwatchSize } from '@library/components/ui/swatch'

export { default as SwatchGroup } from './SwatchGroup.vue'
export { default as SwatchGroupItem } from './SwatchGroupItem.vue'

export type SwatchGroupOrientation = 'horizontal' | 'vertical'

export interface SwatchGroupContext {
  size?: SwatchSize
  shape?: SwatchShape
  orientation: SwatchGroupOrientation
  labelClass?: HTMLAttributes['class']
}

export const SWATCH_GROUP_INJECTION_KEY = Symbol('swatch-group-context') as InjectionKey<SwatchGroupContext>
```
