Skip to content

Switches

A switch is an input widget that allows users to choose one of two values: on or off. Switches are similar to checkboxes and toggle buttons, which can also serve as binary inputs. One difference, however, is that switches can only be used for binary input.

The switch field has enough unique behaviors and use-cases that justify it having its own composable. The binary state of a switch means it shouldn’t be used to represent “required” inputs where the switch needs to be “on”. It is a user preference that can be turned off.

Features

  • You can use an input[type="checkbox"] element as a base or any custom element.
  • Labeling, descriptions, and error message displays are automatically linked to input and label elements with aria-* attributes.
  • Support for custom on and off values.
  • Validation support with native HTML constraint validation or Standard Schema validation.
  • Support for v-model binding.
  • Supported keyboard features:
KeyDescription
Space
Toggles the switch on/off.
Enter
Toggles the switch on/off.

Anatomy

Input
Label
Label
Something went wrong
Error Message

Building a Switch component

The useSwitch composable provides the necessary props and methods to build a switch component. It returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the corresponding DOM elements.

There are two ways to build a switch component:

  • With an input[type="checkbox"] as a base element.
  • Without an input element, like a div or a button.

We will review the two ways to build a switch component in the following examples.

With input[type="checkbox"] base element

We will add some styling to the next example because switches don’t look like one unless we style them. Otherwise, it would just look like a checkbox.

<script setup lang="ts">
import { ref } from 'vue';
import Switch from './Switch.vue';
const isOn = ref(false);
</script>
<template>
<Switch label="Theme" v-model="isOn" />
<div>Switch is: {{ isOn }}</div>
</template>
<script setup lang="ts">
import { useSwitch, type SwitchProps } from '@formwerk/core';
const props = defineProps<SwitchProps>();
const { inputProps, labelProps, isPressed, errorMessage, errorMessageProps } =
useSwitch(props);
</script>
<template>
<div class="switch">
<label v-bind="labelProps">
<div class="switch-control">
<div class="switch-knob"></div>
</div>
<input v-bind="inputProps" type="checkbox" class="sr-only" />
{{ label }}
</label>
<div v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
</div>
</template>
92 collapsed lines
<style scoped>
.switch {
--switch-bg: #57534e;
--color-primary: #10b981;
--switch-readonly-bg: #a8a29e;
--switch-knob-color: #f5f5f4;
--color-text: #333;
--color-error: #f00;
--color-focus: var(--color-primary);
label {
display: inline-flex;
align-items: center;
cursor: pointer;
gap: 0.25rem;
font-size: 14px;
user-select: none;
}
.switch-control {
width: 3rem;
height: 1.5rem;
background-color: var(--switch-bg);
border-radius: 99999px;
transition: background-color 0.2s;
display: flex;
align-items: center;
}
.switch-knob {
border-radius: 99999px;
height: 1.2rem;
width: 1.4rem;
background: var(--switch-knob-color);
translate: 2px 0;
transition: translate 0.2s;
}
&:has(:checked) {
.switch-control {
background-color: var(--color-primary);
}
.switch-knob {
translate: 100% 0;
}
}
&:has(:focus) {
.switch-control {
outline: 2px solid var(--color-focus);
}
}
&:has([readonly='true']) {
.switch-control {
background-color: var(--switch-readonly-bg);
}
}
&:has(:disabled) {
.switch-control {
opacity: 0.4;
cursor: not-allowed;
}
}
/** This is a common utility CSS class, you can find it in your CSS framework of choice */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.error {
color: var(--color-error);
display: none;
font-size: 13px;
}
&:has(:user-invalid) {
.error {
display: block;
}
}
}
</style>

Without input elements

Similar to the previous example, we can achieve the same look and behavior with a div element or a button element.

In this example, we will use an svg element to closely resemble a switch. We will borrow those SVG paths from Phosphor Icons.

<script setup lang="ts">
import { ref } from 'vue';
import Switch from './Switch.vue';
const isOn = ref(false);
</script>
<template>
<Switch label="Theme" v-model="isOn" />
<div>Switch is: {{ isOn }}</div>
</template>
<script setup lang="ts">
import { useSwitch, type SwitchProps } from '@formwerk/core';
const props = defineProps<SwitchProps>();
const { inputProps, labelProps, isPressed, errorMessage, errorMessageProps } =
useSwitch(props);
</script>
<template>
<div class="switch">
<div class="switch-wrapper" v-bind="inputProps">
<div class="switch-control">
<div class="switch-knob"></div>
</div>
<div class="switch-label" v-bind="labelProps">{{ label }}</div>
</div>
<div v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
</div>
</template>
96 collapsed lines
<style scoped>
.switch {
--switch-bg: #57534e;
--switch-active-bg: #10b981;
--switch-readonly-bg: #a8a29e;
--switch-knob-color: #f5f5f4;
--color-text: #333;
--color-error: #f00;
--color-primary: #10b981;
--color-focus: var(--color-primary);
.switch-label {
font-size: 14px;
user-select: none;
}
.switch-control {
width: 3rem;
height: 1.5rem;
background-color: var(--switch-bg);
border-radius: 99999px;
transition: background-color 0.2s;
display: flex;
align-items: center;
}
.switch-knob {
border-radius: 99999px;
height: 1.2rem;
width: 1.4rem;
background: var(--switch-knob-color);
translate: 2px 0;
transition: translate 0.2s;
}
[aria-checked='true'] {
.switch-control {
background-color: var(--switch-active-bg);
}
.switch-knob {
translate: 100% 0;
}
}
.switch-wrapper {
display: inline-flex;
align-items: center;
cursor: pointer;
gap: 0.25rem;
&:focus {
.switch-control {
outline: 2px solid var(--color-primary);
}
}
[readonly='true'] {
.switch-control {
background-color: var(--switch-readonly-bg);
}
}
[disabled='true'] {
.switch-control {
opacity: 0.4;
cursor: not-allowed;
}
}
}
/** This is a common utility CSS class, you can find it in your CSS framework of choice */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.error {
color: var(--color-error);
display: none;
font-size: 13px;
}
&[aria-invalid='true'] {
.error {
display: block;
}
}
}
</style>

Validation

HTML Constraints

While ideally, the switch field should not be validatable, you can still use the required attribute if you choose to use an input[type="checkbox"] as a base element. We make no assumptions about how you want to use the switch field.

NameTypeDescription
requiredbooleanWhether the number field is required.
<script setup lang="ts">
import Switch from './Switch.vue';
</script>
<template>
<Switch label="HTML Validation" required />
</template>

Standard Schemas

useSwitch also supports Standard Schema validation through the schema prop. This includes multiple providers like Zod, Valibot, Arktype, and more.

<script setup lang="ts">
import { z } from 'zod';
import Switch from './Switch.vue';
const schema = z.literal(true);
</script>
<template>
<Switch label="Standard Schema" :schema="schema" />
</template>

Mixed Validation

While it is unlikely that you need both HTML constraints and Standard Schemas to validate a switch, Formwerk supports mixed validation, which means you can use both HTML constraints and Standard Schemas to validate the switch, and they will work together.

Note that HTML constraints are validated first, so any errors from the HTML constraints will be displayed first. Then, once all HTML constraints are satisfied, the Standard Schema is validated.

<script setup lang="ts">
import { z } from 'zod';
import Switch from './Switch.vue';
const schema = z.custom((val) => {
return val !== true;
}, 'I am a paradox');
</script>
<template>
<Switch label="Mixed Validation" required :schema="schema" />
</template>

If you need to disable the native validation, you can do so by setting the disableHtmlValidation prop to true. You can also disable it globally for all fields. For more information, check out the Validation guide.

<script setup lang="ts">
import { z } from 'zod';
import Switch from './Switch.vue';
const schema = z.custom((val) => {
return val !== true;
}, 'I am a paradox');
</script>
<template>
<Switch
label="Mixed Validation"
required
:schema="schema"
disable-html-validation
/>
</template>

Usage

Disabled

Use disabled to mark the switch as non-interactive. Disabled switches are not validated and will not be submitted.

If you need to prevent the user from interacting with the field while still allowing it to submit, consider using readonly instead.

<script setup lang="ts">
import Switch from './Switch.vue';
</script>
<template>
<Switch label="Disabled switch" disabled />
</template>

Readonly

Readonly switches are validated and submitted, but they do not accept user input. The switch is still focusable, and the value is copyable. For more info, check the MDN.

<script setup lang="ts">
import { ref } from 'vue';
import Switch from './Switch.vue';
const value = ref(true);
</script>
<template>
<Switch label="Readonly switch" v-model="value" readonly />
</template>

Custom On/Off values

You can customize the on and off values of the switch component by passing the trueValue and falseValue props. They can be anything you want.

<script setup lang="ts">
import { ref } from 'vue';
import Switch from './Switch.vue';
const value = ref('off');
</script>
<template>
<Switch
label="Custom values"
v-model="value"
:trueValue="'on'"
:falseValue="'off'"
/>
<div>Value is: {{ value }}</div>
</template>

API

Props

These are the properties that can be passed to the useSwitch composable.

NameTypeDescription
disabledWhether the switch is disabled.
disableHtmlValidationWhether to disable HTML5 validation.
falseValueThe value to use when the switch is unchecked.
labelThe label text for the switch.
modelValueThe v-model value of the switch.
nameThe name attribute for the switch input.
readonlyWhether the switch is readonly.
requiredWhether the switch is required.
schemaSchema for switch validation.
trueValueThe value to use when the switch is checked.

Returns

These are the properties in the object returned by the useSwitch composable.

NameTypeDescription
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
Props for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
inputElReference to the input element.
inputProps
Ref<SwitchDomInputProps | SwitchDOMProps>
Props for the input element.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isPressedWhether the switch is pressed.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsProps for the label element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
togglePressedToggles the pressed state of the switch.