Controls
The three small inputs a user flips: a checkbox, a radio, a switch. Checkbox and Radio are styled native <input>s, Toggle is a headlessui Switch drawn as a pill. None of them render their own label, so pair each with a <span> inside a <label> (or with FormLabel inside a FormItem). One surprise before you start: Checkbox does not take v-model — it takes :checked and @change. See the asymmetry below.
ts
import { Checkbox, Radio, Toggle } from '@scaler-tech/aurora/controls'Checkbox
Radio
Toggle
States
Use when
| Situation | Use | Why |
|---|---|---|
| A setting that takes effect the moment it flips | Toggle | reads as a switch, not as pending form state |
| A boolean the user submits later with a form | Checkbox | belongs to the form, not to the app state |
| Several checkboxes, any number selectable | Checkbox | multi-select is what a checkbox means |
| One of a few options, all visible, in a form | Radio | mutually exclusive, keyboard-groupable |
| One of a few options as a view/mode switch | SegmentedControl | the options are the UI, not a form field |
| One of many options, or the list is long | Listbox / Combobox | radios stop scaling past ~5 options |
The v-model asymmetry (read this first)
Only two of the three take v-model. Checkbox has no defineModel and no update:modelValue emit at all — it is a bare native input, so v-model binds to nothing and the box never updates.
| Component | State binding | Notes |
|---|---|---|
Checkbox | :checked + @change | no v-model, no modelValue |
Radio | v-model + :value | defineModel<string | number | boolean | null>() |
Toggle | v-model | defineModel<boolean>({ default: false }) |
API
Checkbox
No declared props. Everything falls through to the native <input type="checkbox">, so the API is the DOM's: checked, disabled, indeterminate, name, value, required, aria-*. class is merged with tailwind-merge, so a utility you pass wins over the default. Note that indeterminate is a DOM property with no HTML attribute — it works because Vue sets it as a property, and it stays true until you set it back; ticking the box does not clear it for you.
Radio
No declared props either — same fall-through — plus the model.
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | string | number | boolean | null | undefined | The selected value, shared by every radio in the group |
value | any | — | This radio's own value; required, or nothing can be selected |
name | string | — | Groups the inputs for arrow-key navigation |
Toggle
Renders a <button role="switch"> with no accessible name of its own, so wrap it in a <label> with visible text or pass aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | boolean | false | On/off state |
disabled | boolean | false | Declared prop (not fall-through) — greys the track and blocks the click |
Examples
vue
<label class="flex cursor-pointer items-center gap-2">
<Checkbox :checked="agreed" @change="agreed = !agreed" />
<span class="paragraph-sm text-primary">I agree to the terms and conditions</span>
</label>
<label v-for="option in frequencies" :key="option.value" class="flex cursor-pointer items-center gap-2">
<Radio v-model="frequency" name="frequency" :value="option.value" />
<span class="paragraph-sm text-primary">{{ option.label }}</span>
</label>
<label class="flex cursor-pointer items-center gap-2">
<Toggle v-model="notifications" />
<span class="paragraph-sm text-primary">Email me product updates</span>
</label>
<!-- A parent checkbox over a group of children takes the native indeterminate property -->
<Checkbox
:checked="state === 'checked'"
:indeterminate="state === 'indeterminate'"
@change="toggleAll()"
/>Incorrect / Correct
Incorrect (v-model on a Checkbox — it renders, it just never changes):
vue
<Checkbox v-model="isInternal" />Correct (drive the native input with :checked and flip it in @change):
vue
<Checkbox :checked="isInternal" @change="isInternal = !isInternal" />Gotchas
- A
Checkboxwithv-modelshows a checkmark on click and never writes to your ref — that checkmark is the browser's uncontrolled state, and the next re-render snaps it back silently. @changefires with the DOMEvent, not a boolean. Flip your own state (@change="x = !x") or read($event.target as HTMLInputElement).checked.- Radios that all light up together, or a model that comes back as
"on", means:valueis missing — give each radio its own:value, one sharedv-model, and a sharedname. - A checked
CheckboxorRadiofills with the surrounding text colour:@tailwindcss/formspaintscurrentColoron:checked[external], so change it withtext-*—bg-*loses.