Skip to content

@vibe-labs/design-vue-toggles

Vue 3 toggle switch component for the Vibe Design System.

Installation

ts
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

Requires the CSS layer from @vibe-labs/design-components-toggles.


Component

VibeToggle

Toggle switch with label support, native form submission, and proper role="switch" ARIA semantics.

Usage

vue
<!-- Basic -->
<VibeToggle v-model="enabled" label="Enable notifications" />

<!-- Without label -->
<VibeToggle v-model="darkMode" aria-label="Dark mode" />

<!-- Label on the left -->
<VibeToggle v-model="active" label="Active" label-position="left" />

<!-- Sizes -->
<VibeToggle v-model="val" size="sm" label="Small" />
<VibeToggle v-model="val" size="md" label="Medium" />
<VibeToggle v-model="val" size="lg" label="Large" />

<!-- Disabled -->
<VibeToggle v-model="locked" label="Locked setting" disabled />

<!-- In a form (native submission) -->
<form @submit="onSubmit">
  <VibeToggle v-model="agreed" name="terms" value="accepted" label="I agree to the terms" required />
</form>

<!-- External label -->
<label id="my-label">WiFi</label>
<VibeToggle v-model="wifi" aria-labelledby="my-label" />

<!-- With description -->
<VibeToggle v-model="sync" label="Auto-sync" aria-describedby="sync-desc" />
<p id="sync-desc">Automatically sync changes every 5 minutes.</p>

Props

PropTypeDefaultDescription
modelValuebooleanfalseChecked state (v-model)
sizeToggleSize"md"sm · md · lg
disabledbooleanfalseDisabled state
labelstringLabel text beside the toggle
labelPosition"left" | "right""right"Label placement
namestringForm field name
valuestring"on"Submitted value when checked
requiredbooleanfalseRequired field
idstringautoInput element ID
ariaLabelstringAccessible label (when no visible label)
ariaLabelledbystringExternal label element ID
ariaDescribedbystringDescribing element ID

Events

EventPayloadDescription
update:modelValuebooleanToggle state changed
changebooleanToggle state changed

Accessibility

Uses role="switch" with aria-checked — the correct ARIA pattern for toggle switches (not just a styled checkbox). The hidden <input type="checkbox"> provides native form semantics and keyboard activation (Space to toggle).


Dependencies

PackagePurpose
@vibe-labs/design-components-togglesCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.


Usage Guide

Setup

ts
import { VibeToggle } from "@vibe-labs/design-vue-toggles";
import "@vibe-labs/design-components-toggles";

VibeToggle — Practical Examples

Notification Preferences Form

vue
<script setup lang="ts">
import { reactive } from "vue";
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

const prefs = reactive({
  emailNotifications: true,
  pushNotifications: false,
  weeklyDigest: true,
  marketingEmails: false,
});

async function savePreferences() {
  await fetch("/api/preferences", {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(prefs),
  });
}
</script>

<template>
  <form @submit.prevent="savePreferences">
    <fieldset>
      <legend>Notification Preferences</legend>

      <VibeToggle
        v-model="prefs.emailNotifications"
        label="Email notifications"
        size="md"
      />
      <VibeToggle
        v-model="prefs.pushNotifications"
        label="Push notifications"
      />
      <VibeToggle
        v-model="prefs.weeklyDigest"
        label="Weekly digest"
      />
      <VibeToggle
        v-model="prefs.marketingEmails"
        label="Marketing emails"
      />
    </fieldset>

    <button type="submit">Save</button>
  </form>
</template>

Dark Mode Toggle

vue
<script setup lang="ts">
import { ref, watch } from "vue";
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

const isDark = ref(document.documentElement.classList.contains("dark"));

watch(isDark, (dark) => {
  document.documentElement.classList.toggle("dark", dark);
  localStorage.setItem("theme", dark ? "dark" : "light");
});
</script>

<template>
  <VibeToggle
    v-model="isDark"
    aria-label="Dark mode"
    size="md"
  />
</template>

Toggle with External Description

vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

const autoSync = ref(false);
</script>

<template>
  <div class="setting-row">
    <div class="setting-row__labels">
      <label id="sync-label" class="setting-row__title">Auto-sync</label>
      <p id="sync-desc" class="setting-row__description">
        Automatically sync changes to the server every 5 minutes.
      </p>
    </div>
    <VibeToggle
      v-model="autoSync"
      aria-labelledby="sync-label"
      aria-describedby="sync-desc"
      label-position="left"
    />
  </div>
</template>

Common Patterns

Disabled When Dependent Setting is Off

vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

const notificationsEnabled = ref(false);
const soundEnabled = ref(false);
</script>

<template>
  <div>
    <VibeToggle v-model="notificationsEnabled" label="Enable notifications" />
    <VibeToggle
      v-model="soundEnabled"
      label="Notification sounds"
      :disabled="!notificationsEnabled"
    />
  </div>
</template>

Loading State During Async Toggle

vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeToggle } from "@vibe-labs/design-vue-toggles";

const enabled = ref(false);
const saving = ref(false);

async function handleChange(value: boolean) {
  saving.value = true;
  await fetch("/api/feature-flag", {
    method: "PATCH",
    body: JSON.stringify({ enabled: value }),
  });
  enabled.value = value;
  saving.value = false;
}
</script>

<template>
  <VibeToggle
    :model-value="enabled"
    label="Feature flag"
    :disabled="saving"
    @change="handleChange"
  />
</template>

Form Submission with Native Checkbox Value

vue
<template>
  <form method="POST" action="/api/settings">
    <VibeToggle
      :model-value="terms"
      name="terms"
      value="accepted"
      label="I agree to the terms of service"
      required
      @change="(v) => terms = v"
    />
    <button type="submit">Continue</button>
  </form>
</template>

Vibe