Switch
A toggle switch component for boolean states
Usage
<script setup lang="ts">
import { Switch, SwitchGroup } from '@hareui/vue'
</script><script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <Switch label="Enable notifications" /></template>Anatomy
Switch is a single component — no compound parts. Label, description, and the thumb icon are composed through props or named slots.
<Switch label="…" description="…" error-message="…">
<template #label="{ checked }" /> <!-- Label content -->
<template #description /> <!-- Description content -->
<template #icon="{ checked }" /> <!-- Icon rendered inside the thumb -->
<template #error="{ validationErrors }" /> <!-- FieldError content, rendered while invalid -->
</Switch>ui key | data-slot | Element |
|---|---|---|
base | switch | <div> root |
content | switch-content | The Reka SwitchRoot button |
control | switch-control | The track |
thumb | switch-thumb | The moving thumb |
icon | switch-icon | Icon container inside the thumb (iff #icon) |
| — | description | Description (iff description / #description) |
| — | field-error | FieldError (while invalid, iff error-message / #error / validation errors) |
Group multiple switches with SwitchGroup, a layout-only wrapper (orientation: 'vertical' | 'horizontal', default vertical):
<SwitchGroup>
<Switch name="a" label="Option 1" />
<Switch name="b" label="Option 2" />
</SwitchGroup>ui key | data-slot | Element |
|---|---|---|
base | switch-group | <div> root |
items | switch-group-items | Container for the switch items |
Examples
Sizes
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <div class="flex gap-6"> <Switch size="sm" label="Small" /> <Switch size="md" label="Medium" /> <Switch size="lg" label="Large" /> </div></template>With Icons
<script setup lang="ts">import { Icon } from '@iconify/vue'import { Switch } from '@hareui/vue'import { reactive } from 'vue'const icons = [ { key: 'check', off: 'gravity-ui:power', on: 'gravity-ui:check', selectedControlClass: 'bg-green-500/80' }, { key: 'darkMode', off: 'gravity-ui:moon', on: 'gravity-ui:sun', selectedControlClass: '' }, { key: 'microphone', off: 'gravity-ui:microphone', on: 'gravity-ui:microphone-slash', selectedControlClass: 'bg-red-500/80' }, { key: 'notification', off: 'gravity-ui:bell-slash', on: 'gravity-ui:bell-fill', selectedControlClass: 'bg-purple-500/80' }, { key: 'volume', off: 'gravity-ui:volume-fill', on: 'gravity-ui:volume-slash-fill', selectedControlClass: 'bg-blue-500/80' },]const selected = reactive<Record<string, boolean>>( Object.fromEntries(icons.map(icon => [icon.key, true])),)</script><template> <div class="flex gap-3"> <Switch v-for="icon in icons" :key="icon.key" v-model="selected[icon.key]" :aria-label="icon.key" size="lg" :ui="{ control: selected[icon.key] ? icon.selectedControlClass : '' }" > <template #icon="{ checked }"> <Icon :icon="checked ? icon.on : icon.off" class="size-3 text-inherit" :class="checked ? 'opacity-100' : 'opacity-70'" /> </template> </Switch> </div></template>Disabled
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <Switch disabled label="Enable notifications" /></template>Without Label
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <Switch aria-label="Enable notifications" /></template>With Description
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <div class="max-w-sm"> <Switch label="Public profile" description="Allow others to see your profile information" /> </div></template>Default Selected
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <Switch default-value label="Enable notifications" /></template>Controlled
Switch is off
<script setup lang="ts">import { Switch } from '@hareui/vue'import { ref } from 'vue'const isSelected = ref(false)</script><template> <div class="flex flex-col gap-4"> <Switch v-model="isSelected" label="Enable notifications" /> <p class="text-sm text-muted"> Switch is {{ isSelected ? 'on' : 'off' }} </p> </div></template>Label Position
HareUI has no dedicated label-position prop; reverse the order with a content class override:
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <div class="flex flex-col gap-4"> <Switch label="Label after" /> <Switch label="Label before" :ui="{ content: 'flex-row-reverse justify-end' }" /> </div></template>Group
<script setup lang="ts">import { Switch, SwitchGroup } from '@hareui/vue'</script><template> <SwitchGroup> <Switch name="notifications" label="Allow Notifications" /> <Switch name="marketing" label="Marketing emails" /> <Switch name="social" label="Social media updates" /> </SwitchGroup></template>Group Horizontal
<script setup lang="ts">import { Switch, SwitchGroup } from '@hareui/vue'</script><template> <SwitchGroup class="overflow-x-auto" orientation="horizontal"> <Switch name="notifications" label="Notifications" /> <Switch name="marketing" label="Marketing" /> <Switch name="social" label="Social" /> </SwitchGroup></template>Form Integration
<script setup lang="ts">import { Button, Switch, SwitchGroup } from '@hareui/vue'function handleSubmit(e: Event) { e.preventDefault() const formData = new FormData(e.target as HTMLFormElement) alert( `Form submitted with:\n${Array.from(formData.entries()) .map(([key, value]) => `${key}: ${value}`) .join('\n')}`, )}</script><template> <form class="flex flex-col gap-4" @submit="handleSubmit"> <SwitchGroup> <Switch name="notifications" value="on" label="Enable notifications" /> <Switch default-value name="newsletter" value="on" label="Subscribe to newsletter" /> <Switch name="marketing" value="on" label="Receive marketing updates" /> </SwitchGroup> <Button class="mt-4" size="sm" type="submit" variant="primary"> Submit </Button> </form></template>Render Props
The #label, #description and #icon slots receive { checked }:
<script setup lang="ts">import { Switch } from '@hareui/vue'</script><template> <Switch> <template #label="{ checked }"> {{ checked ? 'Enabled' : 'Disabled' }} </template> </Switch></template>Customization
Tailwind CSS
Override the control's checked colors with CSS variables through :ui, and stack a Description under the label inside #label:
<script setup lang="ts">import { Description, Switch } from '@hareui/vue'</script><template> <Switch id="autosave" :ui="{ control: '[--switch-control-bg-checked-hover:var(--success)] [--switch-control-bg-checked:var(--success)]' }" > <template #label> <span class="flex flex-col gap-0.5"> <span>Auto-save drafts</span> <Description>Changes are saved as you type.</Description> </span> </template> </Switch></template>Global Configuration
app.use(createHareUI({
ui: { switch: { slots: { control: 'rounded-md' } } },
}))Styling Reference
Slots
base→[data-slot="switch"]– root container, carriesdata-selected/data-disabled/data-invalid/data-readonly/data-requiredcontent→[data-slot="switch-content"]– the interactiveSwitchRootelementcontrol→[data-slot="switch-control"]– the trackthumb→[data-slot="switch-thumb"]– the moving thumbicon→[data-slot="switch-icon"]– optional icon inside the thumb
Interactive States
- Selected:
[data-selected="true"]on the root (thumb position and background color change) - Hover:
:hoveron the root - Focus:
:focus-visibleon the content element (shows focus ring) - Disabled:
[data-disabled="true"]on the root (reduced opacity, no pointer events) - Pressed:
:activeon the root - Invalid:
[data-invalid="true"]on the root,aria-invalid="true"on the content element - Read only:
[data-readonly="true"]on the root,aria-readonly="true"on the content element - Required:
[data-required="true"]on the root
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | undefined | The checked state. Bind with v-model. |
defaultValue | boolean | false | The initial checked state when uncontrolled. |
label | string | - | The label rendered next to the control. |
description | string | - | Help text rendered below the label. |
errorMessage | string | - | Error message rendered below the label while invalid. Defaults to the validation errors. |
size | "md" | "sm" | "lg" | 'md' | Size of the switch. |
disabled | boolean | false | Whether the switch is disabled. |
invalid | boolean | undefined | Whether the switch is invalid. Sets aria-invalid on the control and data-invalid on the root. Overrides validation when set. |
readonly | boolean | false | Whether the switch is read only: focusable and announced, but its state can't be changed. |
required | boolean | false | Whether user input is required on the switch before form submission. |
validate | ValidateFn<boolean> | - | Validates the selection (true when on). Return an error message (or several) when invalid. |
validationBehavior | ValidationBehavior | 'native' | native blocks form submission and shows errors on change or submit; aria shows errors in realtime.
Defaults to the surrounding Form. |
name | string | - | The name of the switch, used when submitting an HTML form. |
value | string | 'on' | The value of the switch, used when submitting an HTML form. |
id | string | - | The id of the switch control. Generated when omitted; use it to point an external Label at the switch. |
ui | ComponentSlots<{ slots: { base: string[]; content: string[]; control: string[]; thumb: string[]; icon: string; }; variants: { size: { sm: { base: string; control: string; thumb: string; }; md: {}; lg: { base: string; control: string; thumb: string; }; }; }; defaultVariants: { size: string; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
label | { checked: boolean; } | The label content. Replaces the label prop. |
description | { checked: boolean; } | The description content. Replaces the description prop. |
error | ValidationResult | The error content, rendered while invalid. Replaces the errorMessage prop. |
icon | { checked: boolean; } | Icon rendered inside the thumb. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: boolean] | - |
SwitchGroup
SwitchGroup is a layout-only wrapper: it renders no state of its own and shares no context with its Switch children — each switch keeps its own name/value/v-model. Use it purely to apply the grouped spacing/orientation styling HeroUI ships for stacks of switches.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | 'vertical' | The axis the switches are laid out along. |
ui | ComponentSlots<{ slots: { base: string; items: string; }; variants: { orientation: { horizontal: { items: string; }; vertical: { items: string; }; }; }; defaultVariants: { orientation: string; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
default | any | The Switch items to render. |
Accessibility
- Clicking anywhere in the content area (including the label) toggles the switch
- Use
aria-labelwhen no visiblelabel/#labelis provided descriptionis linked viaaria-describedbyrequiredanddisabledset the native attributes on the underlying control







