Keyboard shortcut: Ctrl + K
Get started

Checkbox

A choice that a submit will carry out, as opposed to a switch, which acts at once. It wraps a real <input type="checkbox">, so it submits with the form.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const subscribed = ref(true)
</script>

<template>
  <VCheckbox v-model="subscribed">Send me release notes</VCheckbox>
</template>

Examples

Hint

label writes the text beside the box, and the default slot replaces it when the label needs more than text. hint adds a caption underneath, tied to the box through aria-describedby and kept outside the <label>, so it is announced as a description rather than as part of the name.

vue
A summary of the week, every Monday.Only when someone names you.
<script setup lang="ts">
import { ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const digest = ref(true)
const mentions = ref(false)
</script>

<template>
  <div class="stack">
    <!-- The hint is tied to the box through aria-describedby, and it sits outside the
         label, so a screen reader reads it after the name rather than as part of it. -->
    <VCheckbox v-model="digest" label="Weekly digest" hint="A summary of the week, every Monday." />
    <VCheckbox v-model="mentions" label="Mentions" hint="Only when someone names you." />
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-3);
}
</style>

Label position

labelPosition moves the label before the box instead of after it.

vue
<script setup lang="ts">
import { VCheckbox } from 'vectis-ui'
</script>

<template>
  <div class="stack">
    <VCheckbox>Label after the box, the default</VCheckbox>
    <VCheckbox label-position="start">Label before the box</VCheckbox>
  </div>
</template>

<style scoped>
/* `justify-items: start` keeps each row as wide as its own label: the clickable area is
   the whole <label>, so a stretched row would be clickable well past its text. */
.stack {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-2);
}
</style>

Spread

spread takes the full width offered and pushes the label and the box to opposite ends of the row.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const analytics = ref(true)
const crashes = ref(false)
</script>

<template>
  <div class="settings">
    <VCheckbox v-model="analytics" spread>Share anonymous usage data</VCheckbox>
    <VCheckbox v-model="crashes" spread label-position="start">Send crash reports</VCheckbox>
  </div>
</template>

<style scoped>
/* A spread row takes the width it is given, so the panel is what decides how far apart
   the label and the box end up. */
.settings {
  display: grid;
  gap: var(--vectis-space-3);
  inline-size: 100%;
  max-inline-size: 24rem;
  padding: var(--vectis-space-4);
  border: 1px solid var(--vectis-color-border);
  border-radius: var(--vectis-radius-surface);
}
</style>

Indeterminate

indeterminate shows a dash instead of a tick. It is an appearance of its own: the v-model still holds true or false.

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const scopes = ref([
  { label: 'Read', granted: true },
  { label: 'Write', granted: false },
  { label: 'Delete', granted: false },
])

const all = computed({
  get: () => scopes.value.every((scope) => scope.granted),
  set: (value: boolean) => {
    scopes.value.forEach((scope) => {
      scope.granted = value
    })
  },
})

/* The dash is a third appearance and not a third value: the parent is genuinely unticked
   here, and `indeterminate` is what draws it as partially checked. */
const some = computed(() => scopes.value.some((scope) => scope.granted) && !all.value)
</script>

<template>
  <div class="tree">
    <VCheckbox v-model="all" :indeterminate="some">Permissions</VCheckbox>
    <div class="children">
      <VCheckbox v-for="scope in scopes" :key="scope.label" v-model="scope.granted">
        {{ scope.label }}
      </VCheckbox>
    </div>
  </div>
</template>

<style scoped>
.tree,
.children {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-2);
}
.children {
  padding-inline-start: var(--vectis-space-6);
}
</style>

Read-only

readonly shows the state without letting it change. The native attribute does nothing on a checkbox, so the component cancels the click, which covers Space as well. The box stays focusable, is submitted with its form and is announced as read-only. It still takes part in constraint validation, though, so readonly with required and nothing ticked leaves a form that cannot be submitted or fixed.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const on = ref(true)
const off = ref(false)
</script>

<template>
  <div class="stack">
    <!-- Still focusable and still submitted with the form, but a click or the Space key
         changes nothing. -->
    <VCheckbox v-model="on" readonly label="Set by your organisation" />
    <VCheckbox v-model="off" readonly label="Not available on your plan" />
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-3);
}
</style>

Disabled

disabled prevents the box from being ticked and greys it out through the colour tokens. The keyboard steps over it.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VCheckbox } from 'vectis-ui'

const off = ref(false)
const on = ref(true)
</script>

<template>
  <div class="stack">
    <VCheckbox v-model="off" disabled>Unticked</VCheckbox>
    <VCheckbox v-model="on" disabled>Ticked</VCheckbox>
    <VCheckbox v-model="off" disabled indeterminate>Partially ticked</VCheckbox>
  </div>
</template>

<style scoped>
.stack {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-2);
}
</style>

API

Props

PropTypeDefault
labelstringnone
The text beside the box, which names it. The default slot replaces it.
hintstringnone
A line of help under the label. It is tied to the checkbox for assistive technology, so it is read out after the label rather than as part of it.
indeterminatebooleanfalse
Shows the box as partially checked, a dash instead of a tick. This is what a parent checkbox looks like when some of its children are ticked and others are not. It is a state of its own, not a value the v-model can hold.
labelPositionCheckboxLabelPosition'start' | 'end''end'
Which side of the box the label sits on.
spreadbooleanfalse
Pushes the label and the box to opposite ends of the line, the row taking the full width available. This is the usual shape for a list of settings.
invalidbooleanfalse
Marks the field as invalid, which colours the box and tells assistive technology so. Use it for a rule the browser cannot check by itself; native validity is already handled without it.
disabledbooleanfalse
Makes the checkbox unusable, greyed out through the colour tokens.
readonlybooleanfalse
Shows the state without allowing it to be changed. The checkbox can still be focused, is announced as read-only and is still submitted with its form; a click or the Space key simply changes nothing.
v-modelbooleanfalse
Whether the box is ticked. It starts unticked, and the dash is a third appearance rather than a third value: that one is indeterminate.

Slots

SlotType
default{}
The label, when it needs more than the label prop's text. It is clickable.

CSS variables

TokenValue
--vectis-control-size-check1.25rem
--vectis-control-size-check-mark0.875rem