oriUI

Switch

An on/off toggle built on a real <input type="checkbox" role="switch"> — kept in the DOM (visually hidden over a track + thumb) so keyboard, focus, and form submission work for free, and assistive technology announces it as a switch. v-model is a boolean. The "on" track color and the :focus-visible ring both come from the ori-color token; the track and thumb scale with ori-font-size so size is a single prop.

The examples are organized by layer: the class reference is the standalone @oriui/css layer, and the Framework API is the @oriui/vue component. Every example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte / plain-HTML usage), Vue; HTML is the default.

Classes

A switch is a block class on the <label> wrapper plus two single-class token utilities — one class per axis, no base class needed. The Vue props in Framework API map 1:1 to these.

ClassTypeDescription
ori-switchBlockRequired base class — placed on the wrapping label.
ori-color_*Colorprimary · secondary · success · warning · danger · info · surface
ori-font-size_*Sizexs · sm · md · lg · xl · xxl — the track and thumb scale with this.
ori-switch__inputPartThe visually-hidden native input type=checkbox role=switch.
ori-switch__trackPartThe visible pill track; aria-hidden.
ori-switch__thumbPartThe sliding circle inside the track; aria-hidden.
ori-switch__labelPartOptional visible text label beside the track.
disabled · aria-invalidStatereal attributes on the input, not classes; a disabled input dims its wrapper

À la carte: the classes above ship in @oriui/css/components/switch.css. Import a foundation (@oriui/css/base.css or @oriui/css/tokens.css) first — the token utilities (ori-color_*, ori-size-radius_*, …) live there, not in the component file. The full bundle @oriui/css is the default and already carries both; see à-la-carte imports.

Colors

Every semantic color role. The track fill and the focus ring both use --ori-color.

html
<label class="ori-switch ori-color_primary ori-font-size_md" for="s1">
    <input id="s1" type="checkbox" role="switch" class="ori-switch__input" />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">Primary</span>
</label>
<!-- swap the color class: ori-color_primary → ori-color_danger / ori-color_success / ori-color_warning / ori-color_info -->
vue
<OriSwitch v-model="on" label="Primary" color="primary" />
<OriSwitch v-model="on" label="Danger" color="danger" />
<OriSwitch v-model="on" label="Info" color="info" />

Sizes

xs → xxl. A single ori-font-size_* class scales both the label text and the track/thumb geometry.

html
<!-- track and thumb scale off ori-font-size_* -->
<label class="ori-switch ori-color_primary ori-font-size_sm" for="sm">
    <input id="sm" type="checkbox" role="switch" class="ori-switch__input" />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">sm</span>
</label>
<label class="ori-switch ori-color_primary ori-font-size_xl" for="xl">
    <input id="xl" type="checkbox" role="switch" class="ori-switch__input" />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">xl</span>
</label>
vue
<OriSwitch v-model="on" label="sm" size="sm" />
<OriSwitch v-model="on" label="xl" size="xl" />

States

disabled sets the native disabled on the <input>, and the wrapper dims from it (reduced opacity + not-allowed cursor), so a <fieldset disabled> dims it too. invalid sets aria-invalid="true" on the input. required sets the native required.

html
<label class="ori-switch ori-color_primary ori-font-size_md" for="d1">
    <input id="d1" type="checkbox" role="switch" class="ori-switch__input" disabled />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">Disabled off</span>
</label>
<label class="ori-switch ori-color_primary ori-font-size_md" for="d2">
    <input id="d2" type="checkbox" role="switch" class="ori-switch__input" disabled checked />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">Disabled on</span>
</label>
vue
<OriSwitch v-model="on" label="Disabled off" disabled />
<!-- pass a true initial value to show the on state -->
<OriSwitch :modelValue="true" label="Disabled on" disabled />

Invalid

invalid wires aria-invalid="true" so screen readers announce the error state.

html
<label class="ori-switch ori-color_danger ori-font-size_md" for="inv">
    <input id="inv" type="checkbox" role="switch" class="ori-switch__input" aria-invalid="true" required />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
    <span class="ori-switch__label">Required field</span>
</label>
vue
<OriSwitch v-model="accepted" label="Required field" :invalid="!accepted" color="danger" required />

Without a label

Pass no label prop to render the track alone. Always pair a label-less switch with an external aria-label or a visible text element connected via aria-labelledby.

html
<label class="ori-switch ori-color_primary ori-font-size_md" for="dm">
    <input id="dm" type="checkbox" role="switch" class="ori-switch__input" aria-label="Dark mode" />
    <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
</label>
vue
<OriSwitch v-model="darkMode" aria-label="Dark mode" />

Common patterns

A settings row — switch on the right aligned to a description — is the most common real-world use.

html
<div style="display: flex; flex-direction: column; gap: 0.75rem">
    <label class="ori-switch ori-color_primary ori-font-size_md" for="p1">
        <input id="p1" type="checkbox" role="switch" class="ori-switch__input" />
        <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
        <span class="ori-switch__label">Email notifications</span>
    </label>
    <label class="ori-switch ori-color_secondary ori-font-size_md" for="p2">
        <input id="p2" type="checkbox" role="switch" class="ori-switch__input" />
        <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
        <span class="ori-switch__label">Dark mode</span>
    </label>
    <label class="ori-switch ori-color_primary ori-font-size_md" for="p3">
        <input id="p3" type="checkbox" role="switch" class="ori-switch__input" disabled />
        <span class="ori-switch__track" aria-hidden="true"><span class="ori-switch__thumb"></span></span>
        <span class="ori-switch__label">Auto-save</span>
    </label>
</div>
vue
<div style="display: flex; flex-direction: column; gap: 0.75rem">
    <OriSwitch v-model="emailNotifs" label="Email notifications" color="primary" />
    <OriSwitch v-model="darkMode" label="Dark mode" color="secondary" />
    <OriSwitch v-model="autoSave" label="Auto-save" disabled />
</div>

Accessibility

The accessibility contract holds across every layer — the standalone classes and the Vue component render the same attributes and keyboard behavior.

  • Renders a real <input type="checkbox" role="switch"> so keyboard interaction, form submission, and screen-reader announcements work without any JavaScript bridges.
  • The wrapping <label> is linked to the input by for/id (auto-generated via useId() unless id is supplied) — clicking the label text or the track both toggle the switch.
  • The visual track and thumb are aria-hidden="true"; the input is the only element in the accessibility tree.
  • disabled is the real native attribute (not a class); invalid sets aria-invalid="true".
  • A label-less switch must receive an accessible name via aria-label or aria-labelledby passed as an attribute (it falls through to the <input>).
  • :focus-visible outline appears on the track (using --ori-color) so the focus indicator is always visible without disrupting the unfocused appearance.
KeyAction
SpaceToggles the switch on/off.
TabMoves focus to the switch.

Framework API

The props, events, and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above.

Props

PropTypeDefaultDescription
colorThemeColor'primary'Semantic color for the "on" track fill and the focus ring.
disabledbooleanfalseDisables the control — sets native disabled; the wrapper dims from it.
idstring—Overrides the auto-generated useId() id that links the <label> to the <input>.
invalidboolean—Sets aria-invalid="true" on the input; pair with a visible error message.
labelstring—Visible text label rendered beside the track. Omit to render the track alone.
requiredboolean—Sets the native required attribute on the input.
sizeActionSize'md'Controls both label text size and track/thumb geometry (xs · sm · md · lg · xl · xxl).

Events & attributes

OriSwitch uses defineModel<boolean>() for two-way binding — bind with v-model to read the checked state and emit update:modelValue when it toggles.

vue
<OriSwitch v-model="enabled" label="Notifications" />

Because inheritAttrs: false is set and v-bind="$attrs" is forwarded to the <input>, all native input attributes and listeners fall through to the real control:

  • @change, @focus, @blur land on the <input>.
  • name, form, aria-describedby, aria-label, data-* pass through transparently.

Slots

SlotFalls back toDescription
defaultlabel propVisible text label rendered inside the native <label>, beside the track. Omit both to render the track alone.