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

# Toggle Group

Toggles the shopper presses one or several of, such as jacket features to filter by; joined into one control, or spaced apart.

## Install

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

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

Used by: [Delivery Slot](https://ui.frontic.com/components/sections/delivery-slot.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 { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
</script>

<template>
  <ToggleGroup type="multiple" variant="outline" :default-value="['waterproof', 'insulated']" aria-label="Jacket features">
    <ToggleGroupItem value="waterproof">Waterproof</ToggleGroupItem>
    <ToggleGroupItem value="insulated">Insulated</ToggleGroupItem>
    <ToggleGroupItem value="packable">Packable</ToggleGroupItem>
  </ToggleGroup>
</template>
```

## Anatomy

```vue
<ToggleGroup>
  <ToggleGroupItem />
</ToggleGroup>
```

## API reference

### ToggleGroup

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

`data-slot="toggle-group"` · `ToggleGroup.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` | `DataOrientation`: `'vertical' \| 'horizontal'` |  | The orientation of the component, which determines how focus moves: `horizontal` for left/right arrows and `vertical` for up/down arrows. |
| `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. |
| `variant` | `ToggleVariant`: `'default' \| 'outline'` |  | The visual style of every item: transparent or outlined. |
| `size` | `ToggleSize`: `'md' \| 'sm' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` |  | The size of every item. |
| `spacing` | `ToggleGroupSpacing`: `0 \| 1 \| 2 \| 3 \| 4` | `0` | The gap between items, from none to 4 steps; at 0 the items join into one control. |

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

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ modelValue: AcceptableValue \| AcceptableValue[] \| undefined; }` | Holds the group's items; gets the pressed value, or the pressed values when several can be pressed. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-join-x` |  | Present when the items sit flush side by side: `spacing` is 0. |
| `data-join-y` |  | Present when the items sit flush, stacked: `spacing` is 0. |
| `data-size` | `'md' \| 'sm' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | Carries the `size` prop. |
| `data-spacing` |  | Carries the `spacing` prop. |
| `data-variant` | `'default' \| 'outline'` | Carries the `variant` prop. |
| `data-vertical` |  | Present when the part is laid out vertically. |

### ToggleGroupItem

Lets the user switch one option of its group on or off.

`data-slot="toggle-group-item"` · `ToggleGroupItem.vue`

| 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. |
| `variant` | `ToggleVariant`: `'default' \| 'outline'` |  | The item's visual style when the group sets none: transparent or outlined. |
| `size` | `ToggleSize`: `'md' \| 'sm' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` |  | The item's size when the group sets none. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ modelValue: boolean; state: DataState; pressed: boolean; disabled: boolean; }` | Holds the item's label or icon; gets whether it is pressed or disabled and its state as `on` or `off`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-size` | `'md' \| 'sm' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | Carries the `size` prop. |
| `data-spacing` |  | The space between the items, from `spacing`. |
| `data-state` | `'on' \| 'off'` | The state the part is in, one value per state. |
| `data-variant` | `'default' \| 'outline'` | Carries the `variant` prop. |

## Exports

```ts
import { type ToggleGroupSpacing } from '@/components/ui/toggle-group'
```

- `ToggleGroupSpacing`: `0 \| 1 \| 2 \| 3 \| 4`

## Styling

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

States with a look of their own: hover, focus, checked and disabled.

## Source

`components/ui/toggle-group/ToggleGroup.vue`

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

<script setup lang="ts">
import type { AcceptableValue, ToggleGroupRootEmits, ToggleGroupRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ToggleSize, ToggleVariant } from '@library/components/ui/toggle'
import { reactiveOmit } from '@vueuse/core'
import { ToggleGroupRoot, useForwardPropsEmits } from 'reka-ui'
import type { ToggleGroupSpacing } from '.'
import { computed, provide, useTemplateRef } from 'vue'
import { cva } from 'class-variance-authority'
import { pickedItem, useFieldControl } from '@library/composables/use-field-control'

const props = withDefaults(
  defineProps<
    ToggleGroupRootProps & {
      class?: HTMLAttributes['class']
      /** The visual style of every item: transparent or outlined. */
      variant?: ToggleVariant
      /** The size of every item. */
      size?: ToggleSize
      /** The gap between items, from none to 4 steps; at 0 the items join into one control. */
      spacing?: ToggleGroupSpacing
    }
  >(),
  {
    spacing: 0,
  },
)

const emits = defineEmits<ToggleGroupRootEmits>()

provide('toggleGroup', {
  variant: props.variant,
  size: props.size,
  spacing: props.spacing,
})

const vertical = computed(() => props.orientation === 'vertical')

const SPACING = ['none', 'xs', 'sm', 'md', 'lg'] as const
const toggleGroup = cva('fui-toggle-group group/toggle-group data-vertical:flex-col data-vertical:items-stretch data-join-x:*:not-first:-ms-px data-join-y:*:not-first:-mt-px', {
  variants: {
    spacing: {
      none: 'fui-toggle-group-none',
      xs: 'fui-toggle-group-xs',
      sm: 'fui-toggle-group-sm',
      md: 'fui-toggle-group-md',
      lg: 'fui-toggle-group-lg',
    },
  },
  defaultVariants: { spacing: 'none' },
})

const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant', 'spacing')
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=toggle-group-item]', (item) => item.dataset.state === 'on'),
})

defineSlots<{
  /** Holds the group's items; gets the pressed value, or the pressed values when several can be pressed. */
  default?: (props: { modelValue: AcceptableValue | AcceptableValue[] | undefined }) => unknown
}>()
</script>

<template>
  <ToggleGroupRoot
    ref="root"
    v-slot="slotProps"
    data-slot="toggle-group"
    :data-size="size"
    :data-variant="variant"
    :data-spacing="spacing"
    :data-vertical="vertical || undefined"
    :data-join-x="(spacing === 0 && !vertical) || undefined"
    :data-join-y="(spacing === 0 && vertical) || undefined"
    v-bind="joined"
    @update:model-value="report"
    :class="[toggleGroup({ spacing: SPACING[spacing] }), props.class]"
  >
    <slot v-bind="slotProps" />
  </ToggleGroupRoot>
</template>
```

`components/ui/toggle-group/ToggleGroupItem.vue`

```vue
<script lang="ts">
/** Lets the user switch one option of its group on or off. */
export default {}
</script>

<script setup lang="ts">
import type { ToggleGroupItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ToggleSize, ToggleVariant } from '@library/components/ui/toggle'
import type { ToggleGroupSpacing } from '.'
import { reactiveOmit } from '@vueuse/core'
import { ToggleGroupItem, useForwardProps } from 'reka-ui'
import { inject } from 'vue'
import { Toggle } from '@library/components/ui/toggle'

const props = defineProps<
  ToggleGroupItemProps & {
    class?: HTMLAttributes['class']
    /** The item's visual style when the group sets none: transparent or outlined. */
    variant?: ToggleVariant
    /** The item's size when the group sets none. */
    size?: ToggleSize
  }
>()

const context = inject<{ variant?: ToggleVariant; size?: ToggleSize; spacing?: ToggleGroupSpacing }>('toggleGroup')

const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant')
const forwardedProps = useForwardProps(delegatedProps)

type DataState = 'on' | 'off'

defineSlots<{
  /** Holds the item's label or icon; gets whether it is pressed or disabled and its state as `on` or `off`. */
  default?: (props: { modelValue: boolean; state: DataState; pressed: boolean; disabled: boolean }) => unknown
}>()
</script>

<template>
  <ToggleGroupItem v-slot="slotProps" v-bind="forwardedProps" as-child>
    <Toggle
      data-slot="toggle-group-item"
      :data-variant="context?.variant || variant"
      :data-size="context?.size || size"
      :data-spacing="context?.spacing"
      :variant="context?.variant || variant"
      :size="context?.size || size"
      :class="[
        'fui-toggle-group-item hover:z-5 focus:z-10 focus-visible:z-10 aria-pressed:z-10 aria-checked:z-10',
        'group-data-join-x/toggle-group:not-first:rounded-s-none group-data-join-x/toggle-group:not-last:rounded-e-none',
        'group-data-join-y/toggle-group:not-first:rounded-t-none group-data-join-y/toggle-group:not-last:rounded-b-none',
        props.class,
      ]"
    >
      <slot v-bind="slotProps" />
    </Toggle>
  </ToggleGroupItem>
</template>
```

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

```ts
export { default as ToggleGroup } from './ToggleGroup.vue'
export { default as ToggleGroupItem } from './ToggleGroupItem.vue'

export type ToggleGroupSpacing = 0 | 1 | 2 | 3 | 4
```
