Switch

Toggles between on and off.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Switch } from '@hina-ui/vue'

  const autoplay = ref(true)
</script>

<template>
  <Switch v-model="autoplay">Autoplay the next episode</Switch>
</template>
vue

Usage

import { Switch } from '@hina-ui/vue'
ts

A switch stands for a setting that takes effect immediately, unlike a checkbox that waits for a submit. v-model binds a boolean, the default slot is the label, and clicking either the label or the track toggles. Attributes it does not declare land on the inner track element.

Value: false

<script setup lang="ts">
  import { ref } from 'vue'
  import { Switch, Stack, Text } from '@hina-ui/vue'

  const nsfw = ref(false)
</script>

<template>
  <Stack gap="sm">
    <Switch v-model="nsfw">Show mature content</Switch>
    <Text size="sm" tone="muted">Value: {{ nsfw }}</Text>
  </Stack>
</template>
vue

Examples

Description

description adds a line of supporting text under the label, one size smaller.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Switch } from '@hina-ui/vue'

  const sync = ref(true)
</script>

<template>
  <Switch
    v-model="sync"
    description="Pick up where you left off when you open the same work on another device."
  >
    Sync reading progress
  </Switch>
</template>
vue

Settings rows

Use control-placement="end" to place the control after the text and block to fill the available width. The description stays below the label. start and end follow the text direction.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Switch } from '@hina-ui/vue'

  const sync = ref(true)
</script>

<template>
  <Switch
    v-model="sync"
    control-placement="end"
    block
    description="Pick up where you left off when you open the same work on another device."
  >
    Sync reading progress
  </Switch>
</template>
vue

Sizes

size is sm, md or lg; the track is 20, 24 or 28 pixels tall and the text follows.

<script setup lang="ts">
  import { Switch, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack gap="sm">
    <Switch size="sm" model-value>Small</Switch>
    <Switch size="md" model-value>Medium</Switch>
    <Switch size="lg" model-value>Large</Switch>
  </Stack>
</template>
vue

States

invalid gives the track a danger-coloured border; disabled disables the whole control.

<script setup lang="ts">
  import { Switch, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack gap="sm">
    <Switch invalid>Must be on to continue</Switch>
    <Switch disabled>Disabled</Switch>
    <Switch disabled model-value>Disabled and on</Switch>
  </Stack>
</template>
vue

Track only

Without a label only the track renders, and it must then be named with aria-label.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Switch, Inline } from '@hina-ui/vue'

  const dark = ref(false)
</script>

<template>
  <Inline gap="sm">
    <Switch v-model="dark" aria-label="Dark mode" />
  </Inline>
</template>
vue

In a form

Inside a FormField the error message is rendered by the field and linked to the switch; validation rules and submission belong to the Form. The switch carries its own text, so the field needs no label, and its value often decides whether another field is required; such a rule goes on the object and names the field the error lands on.

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, Form, FormField, Input, Switch, Text } from '@hina-ui/vue'

  const schema = v.pipe(
    v.object({
      isPublic: v.boolean(),
      summary: v.string(),
    }),
    v.forward(
      v.check(
        input => !input.isPublic || input.summary.trim().length > 0,
        'A public work needs a summary',
      ),
      ['summary'],
    ),
  )

  const values = reactive({ isPublic: true, summary: '' })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="isPublic">
      <Switch v-model="values.isPublic">Public</Switch>
    </FormField>
    <FormField name="summary" label="Summary">
      <Input v-model="values.summary" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">Save</Button>
    <Text v-if="saved" tone="muted" size="sm">Saved: {{ saved }}</Text>
  </Form>
</template>
vue

Behaviour

  • Clicking the label or the track toggles: the thumb slides across and the track changes colour. Tab lands on the track and Space toggles.
  • Hovering anywhere on the control inks the track, pressing deepens it.

Accessibility

  • The track is a button with role="switch" and aria-checked. The root is a label, so the text is the accessible name.
  • Without a label, name the track with aria-label or aria-labelledby.
  • invalid also sets aria-invalid.

API

Props

Prop
Type
Default
Description
modelValue
boolean
false
Whether the switch is on
size
'sm' | 'md' | 'lg'
'md'
Size
description
string
—
Supporting text under the label
controlPlacement
'start' | 'end'
'start'
Position of the control relative to its text
block
boolean
false
Fill the available row width
disabled
boolean
false
Whether the control is disabled
invalid
boolean
false
Whether the control failed validation
class
string
—
Classes appended to the root element

Slots

Slot
Payload
Description
default
—
The label

Events

Event
Payload
Description
update:modelValue
value: boolean
The value changed