Toggle
A headless toggle button with dual-mode support: standalone boolean binding or group selection with single and multi-select.
Usage
The Toggle component supports two modes:
Standalone mode: Use
v-modelonToggle.Rootfor simple boolean on/off stateGroup mode: Wrap in
Toggle.Groupfor single or multi-select with value-based selection
<script setup lang="ts">
import { Toggle } from '@vuetify/v0'
import { shallowRef } from 'vue'
const saved = shallowRef(false)
</script>
<template>
<div class="flex justify-center">
<Toggle.Root
v-model="saved"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-divider font-medium data-[state=on]:bg-primary data-[state=on]:text-on-primary data-[state=on]:border-primary"
>
<svg
class="size-5 data-[state=off]:opacity-40"
:data-state="saved ? 'on' : 'off'"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path
d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"
:fill="saved ? 'currentColor' : 'none'"
/>
</svg>
Bookmark
</Toggle.Root>
</div>
</template>
Anatomy
<script setup lang="ts">
import { Toggle } from '@vuetify/v0'
</script>
<template>
<!-- Standalone -->
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
<!-- Group (single select) -->
<Toggle.Group>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
</Toggle.Group>
<!-- Group (multi select) -->
<Toggle.Group multiple>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
<Toggle.Root>
<Toggle.Indicator />
</Toggle.Root>
</Toggle.Group>
</template>Examples
Toolbar
Use Toggle.Group with multiple to build a formatting toolbar. Each toggle operates independently — any combination can be active.
The quick brown fox jumps over the lazy dog. This sentence demonstrates how your formatting selections apply in real time.
<script setup lang="ts">
import { Toggle } from '@vuetify/v0'
import { ref } from 'vue'
const formatting = ref<string[]>([])
</script>
<template>
<div class="rounded-lg border border-divider overflow-hidden">
<div class="flex items-center gap-1 px-2 py-1.5 border-b border-divider bg-surface-variant/30">
<Toggle.Group v-model="formatting" class="inline-flex gap-2 rounded overflow-hidden" multiple>
<Toggle.Root
class="size-8 flex items-center justify-center py-1 font-bold text-sm text-on-surface-variant rounded data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="bold"
>
B
</Toggle.Root>
<Toggle.Root
class="size-8 flex items-center justify-center py-1 italic text-sm text-on-surface-variant rounded data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="italic"
>
I
</Toggle.Root>
<Toggle.Root
class="size-8 flex items-center justify-center py-1 underline text-sm text-on-surface-variant rounded data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="underline"
>
U
</Toggle.Root>
<Toggle.Root
class="size-8 flex items-center justify-center py-1 line-through text-sm text-on-surface-variant rounded data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="strikethrough"
>
S
</Toggle.Root>
</Toggle.Group>
</div>
<p
class="px-4 py-3 text-sm leading-relaxed"
:class="{
'font-bold': formatting.includes('bold'),
'italic': formatting.includes('italic'),
'underline': formatting.includes('underline'),
'line-through': formatting.includes('strikethrough'),
}"
>
The quick brown fox jumps over the lazy dog. This sentence demonstrates how your formatting selections apply in real time.
</p>
</div>
</template>
View Switcher
Use Toggle.Group with mandatory for mutually exclusive options like layout switchers. The mandatory prop prevents deselecting all items.
<script setup lang="ts">
import { Toggle } from '@vuetify/v0'
import { shallowRef } from 'vue'
const view = shallowRef('grid')
const items = [
{ title: 'Photos', count: 248 },
{ title: 'Documents', count: 53 },
{ title: 'Music', count: 112 },
{ title: 'Videos', count: 37 },
{ title: 'Archives', count: 19 },
{ title: 'Downloads', count: 84 },
]
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-on-surface-variant">{{ items.length }} folders</span>
<Toggle.Group v-model="view" class="inline-flex rounded-lg border border-divider overflow-hidden" mandatory>
<Toggle.Root
class="p-2 transition-colors data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="grid"
>
<svg
class="size-5"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<rect
height="7"
rx="1"
width="7"
x="3"
y="3"
/>
<rect
height="7"
rx="1"
width="7"
x="14"
y="3"
/>
<rect
height="7"
rx="1"
width="7"
x="3"
y="14"
/>
<rect
height="7"
rx="1"
width="7"
x="14"
y="14"
/>
</svg>
</Toggle.Root>
<Toggle.Root
class="p-2 border-l border-divider transition-colors data-[state=on]:bg-primary data-[state=on]:text-on-primary"
value="list"
>
<svg
class="size-5"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<line x1="3" x2="21" y1="6" y2="6" />
<line x1="3" x2="21" y1="12" y2="12" />
<line x1="3" x2="21" y1="18" y2="18" />
</svg>
</Toggle.Root>
</Toggle.Group>
</div>
<div
:class="view === 'grid'
? 'grid grid-cols-3 gap-4'
: 'flex flex-col gap-2'"
>
<div
v-for="item in items"
:key="item.title"
:class="view === 'grid'
? 'flex flex-col items-center gap-2 p-4 rounded-lg border border-divider'
: 'flex items-center justify-between px-4 py-3 rounded-lg border border-divider'"
>
<svg
class="text-on-surface-variant"
:class="view === 'grid' ? 'size-8' : 'size-5'"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
<div :class="view === 'grid' ? 'text-center' : 'flex-1 ml-3'">
<div class="text-sm font-medium">{{ item.title }}</div>
<div class="text-xs text-on-surface-variant">{{ item.count }} items</div>
</div>
</div>
</div>
</div>
</template>
Accessibility
Toggle renders as a native <button> element with proper ARIA attributes:
| Attribute | Value | Description |
|---|---|---|
aria-pressed | true / false | Reflects the pressed state |
aria-disabled | true / absent | Present when disabled |
Group ARIA
| Attribute | Value | Description |
|---|---|---|
role | group | Identifies the toggle group |
aria-orientation | horizontal / vertical | Layout direction |
aria-disabled | true / absent | Present when group is disabled |
Keyboard
| Key | Action |
|---|---|
Space | Toggle pressed state |
Enter | Toggle pressed state (native button behavior) |
Data Attributes
| Attribute | Values | Description |
|---|---|---|
data-state | on / off | CSS styling hook for pressed state |
data-disabled | present / absent | CSS styling hook for disabled state |
data-orientation | horizontal / vertical | Group orientation (on group element) |
Toggle is not a form control. It has no name prop, no hidden input, and no form submission integration. Use Toggle for UI state (bold/italic toolbar, view switchers) and Checkbox for form data that needs to be submitted.
Yes. Standalone Toggle.Root manages a boolean v-model and works independently. Toggle.Group is only needed when you want selection coordination across multiple toggles.
Roving focus is a separate concern. Toggle.Group manages selection state. Focus management can be layered on top with a separate composable when needed.
Toggle.Root
Props
namespace
string | undefinedNamespace for context provision to children (Indicator)
Default: "v0:toggle:root"
groupNamespace
string | undefinedNamespace for connecting to parent Toggle.Group
Default: "v0:toggle:group"
modelValue
boolean | undefinedEvents
update:model-value
[value: boolean]Slots
default
ToggleRootSlotPropsToggle.Group
Props
Events
update:model-value
[value: T | T[]]Slots
default
ToggleGroupSlotPropsToggle.Indicator
Props
namespace
string | undefinedNamespace for context injection from parent Toggle.Root
Default: "v0:toggle:root"
Slots
default
ToggleIndicatorSlotProps