# Toggle Group

Shares one pressed state between a row of toggles, one at a time or several.

Toggle Group puts a set of [`Toggle.Root`](/components/toggle) buttons under
one value, so pressing one can unpress the others. Useful for a choice with a
handful of options that all fit on screen, like text alignment, and for a row
of formatting buttons where several can be on at once.

::component-preview{name="ToggleGroupPreview"}
```vue
<template>
  <div class="flex flex-col items-start gap-4">
    <Toggle.Group v-model="alignment" aria-label="Text alignment" :class="group">
      <Toggle.Root
        v-for="entry in alignments"
        :key="entry.value"
        :class="toggle"
        :value="entry.value"
      >
        {{ entry.label }}
      </Toggle.Root>
    </Toggle.Group>

    <Toggle.Group
      v-model="formats"
      aria-label="Formatting"
      :class="group"
      :options="{ multiple: true }"
    >
      <Toggle.Root
        v-for="entry in formatting"
        :key="entry.value"
        :aria-label="entry.label"
        :class="[toggle, entry.glyphClass]"
        :value="entry.value"
      >
        {{ entry.glyph }}
      </Toggle.Root>
    </Toggle.Group>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Toggle } from '@maas/mirror/vue'

const alignment = ref(['left'])
const formats = ref(['bold'])

const alignments = [
  { value: 'left', label: 'Left' },
  { value: 'centre', label: 'Centre' },
  { value: 'right', label: 'Right' },
]

const formatting = [
  { value: 'bold', label: 'Bold', glyph: 'B', glyphClass: 'font-bold' },
  { value: 'italic', label: 'Italic', glyph: 'I', glyphClass: 'italic' },
  {
    value: 'underline',
    label: 'Underline',
    glyph: 'U',
    glyphClass: 'underline',
  },
]

const group =
  'rounded-[calc(var(--radius-component-md)+0.25rem+1px)] border-surface flex gap-1 border p-1'

const toggle = [
  'inline-flex h-9 min-w-9 items-center justify-center px-3',
  'rounded-component-md border-2 border-[transparent] type-component-sm',
  'text-primary-solid transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-subtle',
  'active:bg-primary-subtle-active',
  'data-[state=on]:bg-primary-solid data-[state=on]:text-primary-on-solid',
  'data-[state=on]:hover:bg-primary-solid-hover',
  'data-[state=on]:active:bg-primary-solid-active',
].join(' ')
</script>
```
::

## Usage guidelines

- Every `Toggle.Root` inside a group needs a `value`, since that is how the
  group tells one from another. A toggle without one throws rather than joining
  the group silently.
- The group is one tab stop and has no name of its own, so give it an
  `aria-label` describing what the toggles have in common.
- Where only one option can be on and one always has to be, use
  [`Radio`](/components/radio) instead. A single-select group can be emptied by
  pressing the toggle that is already on, which a radio group cannot.

## Anatomy

Wrap the toggles. `Toggle.Root` is the same part you would render on its own.

::component-anatomy
---
parts:
  - name: Toggle.Group
    required: true
    description: 'Renders a <div role="group"> and is the roving-focus group.'
    children:
      - name: Toggle.Root
        required: true
        description: 'Renders a <button>. Needs a value inside a group.'
---
::

```vue
<script setup lang="ts">
import { Toggle } from '@maas/mirror/vue'
</script>

<template>
  <Toggle.Group v-model="alignment" aria-label="Text alignment">
    <Toggle.Root value="left">Left</Toggle.Root>
    <Toggle.Root value="centre">Centre</Toggle.Root>
    <Toggle.Root value="right">Right</Toggle.Root>
  </Toggle.Group>
</template>
```

## Examples

### One at a time, or several

The value is always an array. By default the group presses one toggle at a
time, so pressing another swaps it and pressing the pressed one leaves the
array empty.

```vue
<template>
  <Toggle.Group v-model="alignment">
    <Toggle.Root value="left">Left</Toggle.Root>
    <Toggle.Root value="right">Right</Toggle.Root>
  </Toggle.Group>
</template>

<script setup lang="ts">
const alignment = ref(['left'])
</script>
```

Set `multiple` and each toggle keeps its own state, collected into the same
array in document order.

```vue
<template>
  <Toggle.Group v-model="formats" :options="{ multiple: true }">
    <Toggle.Root value="bold" aria-label="Bold">B</Toggle.Root>
    <Toggle.Root value="italic" aria-label="Italic">I</Toggle.Root>
  </Toggle.Group>
</template>
```

An array with more than one value in a single-select group is cut down to its
first entry, and the shortened array is emitted back, so your `v-model` follows
what the group actually holds.

### Moving between the toggles

The group is one tab stop, and the arrow keys move within it. `orientation`
decides which pair moves and is written to `data-orientation`, so lay the group
out from that attribute rather than from a class of your own.

```vue
<template>
  <Toggle.Group :options="{ orientation: 'vertical' }" aria-label="Alignment">
    <Toggle.Root value="left">Left</Toggle.Root>
    <Toggle.Root value="right">Right</Toggle.Root>
  </Toggle.Group>
</template>
```

Arrow keys wrap at the ends. Set `loop` to `false` to stop there instead, and
`dir` to mirror the horizontal keys. Unset, `dir` follows a
`DirectionProvider` or the surrounding `dir` attribute, as described in
[Composition](/components/composition).

### Disabling the group

`disabled` on the group disables every toggle in it, and reaches each one as
`data-disabled` so a single selector covers both.

```vue
<template>
  <Toggle.Group :options="{ disabled: true }">
    <Toggle.Root value="left">Left</Toggle.Root>
  </Toggle.Group>
</template>
```

A single toggle can also be disabled on its own, which keeps the arrow keys
moving past it.

### Inside a Toolbar

A [`Toolbar`](/components/toolbar) is already the roving-focus group for
everything under it, so a group nested inside one hands the arrow keys over
rather than starting a second order. Nothing else changes: the value, the
attributes and the composable all behave the same.

```vue
<template>
  <Toolbar.Root aria-label="Formatting">
    <Toolbar.Button>Undo</Toolbar.Button>
    <Toggle.Group v-model="formats" :options="{ multiple: true }">
      <Toggle.Root value="bold" aria-label="Bold">B</Toggle.Root>
    </Toggle.Group>
  </Toolbar.Root>
</template>
```

### Inside a form

The group renders no input of its own. Each `Toggle.Root` still carries its own
`name`, and renders the visually hidden checkbox it always would, so
[the form sees it](/components/form-integration#form-participation).

```vue
<template>
  <form>
    <Toggle.Group v-model="formats" :options="{ multiple: true }">
      <Toggle.Root name="format" value="bold" aria-label="Bold">B</Toggle.Root>
      <Toggle.Root name="format" value="italic" aria-label="Italic">
        I
      </Toggle.Root>
    </Toggle.Group>
  </form>
</template>
```

### Reaching it from elsewhere

Give the group an `id` and `useMirrorToggleGroup(id)` reads and writes it from
anywhere in the app.

```vue
<script setup lang="ts">
import { useMirrorToggleGroup } from '@maas/mirror/vue'

const { value, toggle, setValue } = useMirrorToggleGroup('alignment')
</script>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorToggleGroup(id)` as the
programmatic API. The toggles it groups are
[`Toggle.Root`](/components/toggle), documented on its own page.

### `Toggle.Group`

Renders a `<div role="group">`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: '[The instance ID](#reaching-it-from-elsewhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The pressed values. `v-model`.
      - label: 'string[] | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: 'Initial pressed values when [uncontrolled](/components/composition#controlled-and-uncontrolled).'
      - label: 'string[]'
      - label: '[]'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: ToggleGroupOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: multiple
        description: 'More than [one toggle](#one-at-a-time-or-several) can be pressed.'
      - label: boolean
      - label: 'false'
  - items:
      - label: disabled
        description: 'Disables [every toggle](#disabling-the-group) in the group.'
      - label: boolean
      - label: 'false'
  - items:
      - label: orientation
        description: 'The [arrow-key axis](#moving-between-the-toggles).'
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
  - items:
      - label: dir
        description: 'Direction for the [horizontal arrow keys](#moving-between-the-toggles).'
      - label: '''ltr'' | ''rtl'''
      - label: inherited
        plaintext: true
  - items:
      - label: loop
        description: Arrow keys wrap at the ends.
      - label: boolean
      - label: 'true'
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The pressed values change, controlled or not.
      - label: 'string[]'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The pressed values.
      - label: 'string[]'
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
  - items:
      - label: multiple
        description: Mirrors `data-multiple`.
      - label: boolean
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
---
::

### `Toggle.Root`

The same part as [`Toggle.Root`](/components/toggle), with two differences
inside a group. `value` stops being optional, and `modelValue` and
`defaultValue` are ignored: the group owns the pressed state. Everything else,
including `name` and `disabled`, works as documented there.

### Composable

`useMirrorToggleGroup(id)` reaches a group from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: value
        description: The pressed values.
      - label: 'ComputedRef<ToggleGroupValue[]>'
        escape: true
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: ComputedRef<ToggleGroupOrientation>
        escape: true
  - items:
      - label: multiple
        description: Mirrors `data-multiple`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: isPressed
        description: Whether the group holds a value.
      - label: '(value: ToggleGroupValue) => boolean'
        escape: true
  - items:
      - label: toggle
        description: Presses or unpresses one value, following `multiple`.
      - label: '(value: ToggleGroupValue) => void'
        escape: true
  - items:
      - label: setValue
        description: Replaces the whole value.
      - label: '(value: ToggleGroupValue[]) => void'
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

None of its own. `Toggle.Root` keeps the three it always has.

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_item_value
        description: A `Toggle.Root` inside a `Toggle.Group` has no `value`.
---
::

## Accessibility

The group is a `role="group"` and each toggle keeps its own `aria-pressed`, so
a screen reader announces the pressed state per button rather than as a
selection. Give the group an `aria-label`; it has no name of its own.

The whole group is one tab stop. Every key below is handled on the focused
toggle, on `keydown`.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Tab
      - label: Moves into the group at the last focused toggle, then out again.
        plaintext: true
  - items:
      - label: '`ArrowRight` / `ArrowLeft`'
        plaintext: true
      - label: 'Horizontal group only. Moves to the next or previous enabled toggle, swapped under `dir="rtl"`. Wraps while `loop` is on.'
        plaintext: true
  - items:
      - label: '`ArrowDown` / `ArrowUp`'
        plaintext: true
      - label: Vertical group only. Moves to the next or previous enabled toggle, with the same wrapping.
        plaintext: true
  - items:
      - label: '`Home` / `End`'
        plaintext: true
      - label: Moves to the first or last enabled toggle, in either orientation, ignoring `loop`.
        plaintext: true
  - items:
      - label: '`Enter` / `Space`'
        plaintext: true
      - label: Presses the focused toggle. Moving focus never presses anything on its own.
        plaintext: true
---
::

Arrow keys move focus without changing the value, which is where a toggle group
differs from [`Radio`](/components/radio). Each toggle is its own button, so
nothing is pressed until the user presses it.
