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 organised 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 .
ori-color_*Colorprimary · secondary · success · warn · 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 .
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.
ori-switch_disabledStateAdded by the component when disabled is true; also sets the native disabled attribute on the input.

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_warn / 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

xsxxl. 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 adds ori-switch_disabled (reduced opacity + not-allowed cursor) on the wrapper. invalid sets aria-invalid="true" on the input. required sets the native required.

html
<label class="ori-switch ori-switch_disabled 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-switch_disabled 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-switch_disabled 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 behaviour.

  • 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. (Svelte bindings are planned.)

Props

PropTypeDefaultDescription
colorThemeColor'primary'Semantic color for the "on" track fill and the focus ring.
disabledbooleanfalseDisables the control — sets native disabled and adds ori-switch_disabled on the wrapper.
idstringOverrides the auto-generated useId() id that links the <label> to the <input>.
invalidbooleanSets aria-invalid="true" on the input; pair with a visible error message.
labelstringVisible text label rendered beside the track. Omit to render the track alone.
requiredbooleanSets 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.