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
andoff
values. - Validation support with native HTML constraint validation or Standard Schema validation.
- Support for
v-model
binding. - Supported keyboard features:
Key | Description |
---|---|
⎵Space | Toggles the switch on/off. |
↩Enter | Toggles the switch on/off. |
Anatomy
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 adiv
or abutton
.
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 { 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 { 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.
Name | Type | Description |
---|---|---|
required | boolean | Whether 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.
Name | Type | Description |
---|---|---|
disabled | Whether the switch is disabled. | |
disableHtmlValidation | Whether to disable HTML5 validation. | |
falseValue | The value to use when the switch is unchecked. | |
label | The label text for the switch. | |
modelValue | The v-model value of the switch. | |
name | The name attribute for the switch input. | |
readonly | Whether the switch is readonly. | |
required | Whether the switch is required. | |
schema | Schema for switch validation. | |
trueValue | The value to use when the switch is checked. |
Returns
These are the properties in the object returned by the useSwitch
composable.
Name | Type | Description |
---|---|---|
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | Props for the error message element. |
errors | The errors for the field. | |
fieldValue | The value of the field. | |
inputEl | Reference to the input element. | |
inputProps | Ref<SwitchDomInputProps | SwitchDOMProps> | Props for the input element. |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isPressed | Whether the switch is pressed. | |
isTouched | Whether the field is touched. | |
isValid | Whether the field is valid. | |
labelProps | Props for the label element. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
setTouched | Sets the touched state for the field. | |
setValue | Sets the value for the field. | |
submitErrorMessage | The error message for the field from the last submit attempt. | |
submitErrors | The errors for the field from the last submit attempt. | |
togglePressed | Toggles the pressed state of the switch. |