Skip to content

Toggle

A button that stays pressed, for a formatting control or a filter chip.

View source View as Markdown

Toggle is a button that stays pressed once activated, and reports that state as aria-pressed. Useful for a formatting control or a filter chip; for a setting the user turns on and off, use Switch.

App.vue
<template>
  <div class="flex items-center gap-1" role="group" aria-label="Formatting">
    <Toggle.Root
      v-for="format in formats"
      :key="format.label"
      v-model="format.pressed"
      :aria-label="format.label"
      :class="[style, format.glyphClass]"
    >
      {{ format.glyph }}
    </Toggle.Root>
  </div>
</template>

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

const style = [
  'inline-flex size-10 items-center justify-center',
  'rounded-component-md border-2 border-[transparent] type-component-md',
  '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(' ')

const formats = ref([
  { label: 'Bold', glyph: 'B', glyphClass: 'font-bold', pressed: true },
  { label: 'Italic', glyph: 'I', glyphClass: 'italic', pressed: false },
  { label: 'Underline', glyph: 'U', glyphClass: 'underline', pressed: false },
])
</script>

Usage guidelines

  • A toggle whose content is a glyph has no accessible name, so give it an aria-label, as every example on this page does.
  • Set name and value together, or neither, since a name without a value submits the browser’s default of on.
  • Inside a Toggle.Group, value is required and the group owns the pressed state.

Anatomy

Render it on its own.

NameRequiredDescription
Toggle.Root true Renders a button, plus a visually hidden checkbox input when name is set.
<script setup lang="ts">
import { Toggle } from '@maas/mirror/vue'
</script>

<template>
  <Toggle.Root v-model="bold" name="format" value="bold" aria-label="Bold">
    B
  </Toggle.Root>
</template>

Examples

Controlled and uncontrolled

Leave modelValue out and the toggle keeps its own state, seeded by defaultValue. Bind v-model and you own it instead, which is decided once at mount.

<template>
  <Toggle.Root default-value aria-label="Bold">B</Toggle.Root>

  <Toggle.Root v-model="italic" aria-label="Italic">I</Toggle.Root>
</template>

<script setup lang="ts">
const italic = ref(false)
</script>

A row of toggles

A formatting row is a set of independent toggles, each with its own state, so the container around them is yours to write. Where the row needs one tab stop and arrow keys, reach for Toolbar.

<template>
  <div role="group" aria-label="Text formatting">
    <Toggle.Root
      v-for="format in formats"
      :key="format.name"
      v-model="format.pressed"
      :aria-label="format.name"
    >
      {{ format.glyph }}
    </Toggle.Root>
  </div>
</template>

<script setup lang="ts">
const formats = ref([
  { name: 'Bold', glyph: 'B', pressed: false },
  { name: 'Italic', glyph: 'I', pressed: false },
])
</script>

Inside a Toggle Group

Nested in a Toggle.Group, a toggle stops owning its own state: the group holds the value, and value becomes how the group tells one toggle from another. It is required there, and a toggle without one throws missing_item_value. modelValue and defaultValue are ignored in a group, so bind v-model to the group instead.

<template>
  <Toggle.Group v-model="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>

Inside a form

Set name and Toggle.Root renders a visually hidden checkbox input, checked while the toggle is on, so the form sees it.

<template>
  <form>
    <Toggle.Root v-model="bold" name="format" value="bold" aria-label="Bold">
      B
    </Toggle.Root>
  </form>
</template>

A toggle rendered outside the <form> it belongs to points at it by id through form.

Styling from state

Paint the pressed state from data-state, which is on the element in both states.

<template>
  <Toggle.Root class="toggle" aria-label="Bold">B</Toggle.Root>
</template>

<style>
.toggle[data-state='on'] {
  background: var(--app-color-primary-bg-solid);
  color: var(--app-color-primary-fg-on-solid);
}

.toggle[data-disabled='true'] {
  background: var(--app-color-disabled-bg-subtle);
}
</style>

API reference

Module. Toggle.Root takes ordinary props and keeps no store of its own; nested in a Toggle.Group it reads the group’s. Toggle.Root takes no part in a Field: it has no readOnly and no required, and inherits nothing from a surrounding Field.Root.

Toggle.Root

Renders a <button>, plus a visually hidden <input type="checkbox"> once name is set.

Props

PropTypeDefault
modelValue
boolean | undefinedundefined
defaultValue
booleanfalse
value
stringundefined
name
stringundefined
form
stringundefined
disabled
booleanfalse

Emits

EmitPayload
update:modelValue
boolean

Slot props

PropType
pressed
boolean
disabled
boolean

Data attributes

AttributeValue
data-state
on | off
data-pressed
true
data-disabled
true

CSS variables

VariableDefault
--mirror-toggle-cursor
pointer
--mirror-toggle-disabled-cursor
not-allowed
--mirror-toggle-user-select
none

Errors

Code
missing_item_value

On its own, Toggle.Root needs no context and no required prop, so nothing throws.

Accessibility

The rendered element is a <button> with aria-pressed, and a non-native toggle gets role="button", tabindex="0" and aria-disabled rather than leaving the tab order.

KeyAction
EnterToggles, on keydown.
SpaceToggles, on keyup. The keydown default is prevented.