Switch
Instant on/off for a setting. On state uses the brand color.
import { Label } from "@/components/ui/label";import { Switch } from "@/components/ui/switch";
export default function Demo() { return ( <div className="flex flex-col gap-4"> <Label className="flex items-center gap-2.5 text-sm"> <Switch defaultChecked /> Keeper on </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch size="sm" /> Small </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch disabled /> Disabled </Label> </div> );}<script setup lang="ts">import { Switch } from "@/components/ui/switch";</script>
<template> <div class="flex flex-col gap-4"> <label class="flex items-center gap-2.5 text-sm"><Switch default-value /> Keeper on</label> <label class="flex items-center gap-2.5 text-sm"><Switch size="sm" /> Small</label> <label class="flex items-center gap-2.5 text-sm"><Switch disabled /> Disabled</label> </div></template><script lang="ts"> import { Switch } from "$lib/components/ui/switch";</script>
<div class="flex flex-col gap-4"> <label class="flex items-center gap-2.5 text-sm"><Switch checked /> Keeper on</label> <label class="flex items-center gap-2.5 text-sm"><Switch size="sm" /> Small</label> <label class="flex items-center gap-2.5 text-sm"><Switch disabled /> Disabled</label></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/switchpnpm dlx shadcn@latest add @edmi-ui/switchyarn dlx shadcn@latest add @edmi-ui/switchbunx shadcn@latest add @edmi-ui/switchPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/switchpnpm dlx shadcn-vue@latest add @edmi-ui/switchyarn dlx shadcn-vue@latest add @edmi-ui/switchbunx shadcn-vue@latest add @edmi-ui/switchPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/switch.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/switch.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/switch.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/switch.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Label } from "@/components/ui/label";import { Switch } from "@/components/ui/switch";
<div className="flex flex-col gap-4"> <Label className="flex items-center gap-2.5 text-sm"> <Switch defaultChecked /> Keeper on </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch size="sm" /> Small </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch disabled /> Disabled </Label></div><script setup lang="ts">import { Switch } from "@/components/ui/switch";</script>
<template> <div class="flex flex-col gap-4"> <label class="flex items-center gap-2.5 text-sm"><Switch default-value /> Keeper on</label> <label class="flex items-center gap-2.5 text-sm"><Switch size="sm" /> Small</label> <label class="flex items-center gap-2.5 text-sm"><Switch disabled /> Disabled</label> </div></template><script lang="ts"> import { Switch } from "$lib/components/ui/switch";</script>
<div class="flex flex-col gap-4"> <label class="flex items-center gap-2.5 text-sm"><Switch checked /> Keeper on</label> <label class="flex items-center gap-2.5 text-sm"><Switch size="sm" /> Small</label> <label class="flex items-center gap-2.5 text-sm"><Switch disabled /> Disabled</label></div>Component source
Section titled “Component source”"use client";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";import { cn } from "cn";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// On uses --brand so live settings read as active at a glance. Sizes: default 40x24, sm 32x18.function Switch({ className, size = "default", elevation, ...props}: SwitchPrimitive.Root.Props & { size?: "sm" | "default"; /** ✦ depth: raised +1 / floating +2 make the thumb rise (never the track). */ elevation?: Elevation;}) { const level = useElevation(elevation, "handle"); const raised = level === "raised" || level === "floating"; return ( <SwitchPrimitive.Root data-slot="switch" data-size={size} className={cn( "peer group/switch relative inline-flex shrink-0 items-center rounded-full bg-input shadow-[inset_0_1px_2px_rgb(0_0_0/0.12)] transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--ring-soft)] aria-invalid:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--destructive-soft)] aria-invalid:outline aria-invalid:outline-1 aria-invalid:-outline-offset-1 aria-invalid:outline-destructive data-[invalid]:outline data-[invalid]:outline-1 data-[invalid]:-outline-offset-1 data-[invalid]:outline-destructive data-[checked]:bg-brand data-[size=default]:h-6 data-[size=default]:w-10 data-[size=sm]:h-[18px] data-[size=sm]:w-8 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50", className, )} {...props} > <SwitchPrimitive.Thumb data-slot="switch-thumb" className={cn( "pointer-events-none ml-[3px] block rounded-full bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.1)] transition-transform group-data-[size=default]/switch:size-[18px] group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-[checked]:translate-x-4 group-data-[size=sm]/switch:data-[checked]:translate-x-3.5", raised && "bg-linear-to-b from-white to-[#eeede9] shadow-thumb", )} /> </SwitchPrimitive.Root> );}
export { Switch };<script setup lang="ts">import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { computed } from "vue"import { reactiveOmit } from "@vueuse/core"import { SwitchRoot, SwitchThumb, useForwardPropsEmits } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
// On uses --brand so live settings read as active at a glance. Sizes: default 40x24, sm 32x18.const props = withDefaults( defineProps<SwitchRootProps & { class?: HTMLAttributes["class"], size?: "sm" | "default", elevation?: Elevation }>(), { size: "default", elevation: undefined },)const emits = defineEmits<SwitchRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "size", "elevation")
const level = useElevation(() => props.elevation, "handle")const raised = computed(() => level.value === "raised" || level.value === "floating")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// A bare `default-value` attribute arrives as "" (Reka types it loosely, so Vue does not boolean-cast it);// read it as true so `<Switch default-value />` renders on, like `:default-value="true"`.const defaultValue = computed(() => ((props.defaultValue as unknown) === "" ? true : props.defaultValue))</script>
<template> <SwitchRoot v-slot="slotProps" data-slot="switch" :data-size="size" v-bind="forwarded" :default-value="defaultValue" :class=" cn( 'peer group/switch relative inline-flex shrink-0 items-center rounded-full bg-input shadow-[inset_0_1px_2px_rgb(0_0_0/0.12)] transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--ring-soft)] aria-invalid:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--destructive-soft)] aria-invalid:outline aria-invalid:outline-1 aria-invalid:-outline-offset-1 aria-invalid:outline-destructive data-[state=checked]:bg-brand data-[size=default]:h-6 data-[size=default]:w-10 data-[size=sm]:h-[18px] data-[size=sm]:w-8 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', props.class, ) " > <SwitchThumb data-slot="switch-thumb" :class="cn( 'pointer-events-none ml-[3px] block rounded-full bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.1)] transition-transform group-data-[size=default]/switch:size-[18px] group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-[state=checked]:translate-x-4 group-data-[size=sm]/switch:data-[state=checked]:translate-x-3.5', raised && 'bg-linear-to-b from-white to-[#eeede9] shadow-thumb', )" > <slot name="thumb" v-bind="slotProps" /> </SwitchThumb> </SwitchRoot></template>export { default as Switch } from "./Switch.vue";<script lang="ts"> import { Switch as SwitchPrimitive } from "bits-ui"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
// On uses --brand so live settings read as active at a glance. Sizes: default 40x24, sm 32x18. let { ref = $bindable(null), class: className, checked = $bindable(false), size = "default", elevation = "auto", ...restProps }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> & { size?: "sm" | "default"; /** ✦ depth: raised +1 / floating +2 make the thumb rise (never the track). */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "handle"); const raised = $derived(level.current === "raised" || level.current === "floating");</script>
<SwitchPrimitive.Root bind:ref bind:checked data-slot="switch" data-size={size} class={cn( "peer group/switch relative inline-flex shrink-0 items-center rounded-full bg-input shadow-[inset_0_1px_2px_rgb(0_0_0/0.12)] transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--ring-soft)] aria-invalid:shadow-[inset_0_1px_2px_rgb(0_0_0/0.12),0_0_0_3px_var(--destructive-soft)] aria-invalid:outline aria-invalid:outline-1 aria-invalid:-outline-offset-1 aria-invalid:outline-destructive data-[state=checked]:bg-brand data-[size=default]:h-6 data-[size=default]:w-10 data-[size=sm]:h-[18px] data-[size=sm]:w-8 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50", className )} {...restProps}> <SwitchPrimitive.Thumb data-slot="switch-thumb" class={cn( "pointer-events-none ml-[3px] block rounded-full bg-white shadow-[0_0_0_1px_rgb(0_0_0/0.1)] transition-transform group-data-[size=default]/switch:size-[18px] group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-[state=checked]:translate-x-4 group-data-[size=sm]/switch:data-[state=checked]:translate-x-3.5", raised && "bg-linear-to-b from-white to-[#eeede9] shadow-thumb" )} /></SwitchPrimitive.Root>import Root from "./switch.svelte";
export { Root, // Root as Switch,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Only the thumb rises (raised +1 or floating +2); the track never gets a bevel.
Flat (0)
Raised (+1)
Flat (0)
Raised (+1)
Flat (0)
Raised (+1)
import { Label } from "@/components/ui/label";import { Switch } from "@/components/ui/switch";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <div className="flex flex-wrap items-center gap-6"> <Label className="flex items-center gap-2.5 text-sm"> <Switch elevation={value} defaultChecked /> Keeper on </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch elevation={value} size="sm" defaultChecked /> Small </Label> <Label className="flex items-center gap-2.5 text-sm"> <Switch elevation={value} /> Off </Label> </div> </div> ))} </div> );}<script setup lang="ts">import { Switch } from "@/components/ui/switch";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <div class="flex flex-wrap items-center gap-6 text-sm"> <label class="flex items-center gap-2.5"><Switch :elevation="level.value" :default-value="true" /> Keeper on</label> <label class="flex items-center gap-2.5"><Switch :elevation="level.value" size="sm" :default-value="true" /> Small</label> <label class="flex items-center gap-2.5"><Switch :elevation="level.value" /> Off</label> </div> </div> </div></template><script lang="ts"> import { Switch } from "$lib/components/ui/switch";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <div class="flex flex-wrap items-center gap-6 text-sm"> <label class="flex items-center gap-2.5"><Switch elevation={level.value} checked /> Keeper on</label> <label class="flex items-center gap-2.5"><Switch elevation={level.value} size="sm" checked /> Small</label> <label class="flex items-center gap-2.5"><Switch elevation={level.value} /> Off</label> </div> </div> {/each}</div>Built on Base UI Switch.
| Prop | Type | Default |
|---|---|---|
size |
"sm" | "default" |
"default" |
checked / defaultChecked |
boolean |
false |
onCheckedChange |
(checked: boolean, event) => void |
- |
disabled |
boolean |
false |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Checked track uses
--brand(not--primary) so live settings read as active.