Toggle
Two-state button with a pressed state.
import { Toggle } from "@/components/ui/toggle";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <div className="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"> <IconPlaceholder lucide="BoldIcon" tabler="IconBold" hugeicons="TextBoldIcon" phosphor="TextBIcon" remixicon="RiBold" /> </Toggle> <Toggle aria-label="Bold" defaultPressed> <IconPlaceholder lucide="BoldIcon" tabler="IconBold" hugeicons="TextBoldIcon" phosphor="TextBIcon" remixicon="RiBold" /> </Toggle> <Toggle variant="outline" aria-label="Italic"> <IconPlaceholder lucide="ItalicIcon" tabler="IconItalic" hugeicons="TextItalicIcon" phosphor="TextItalicIcon" remixicon="RiItalic" /> </Toggle> <Toggle variant="outline"> <IconPlaceholder lucide="StarIcon" tabler="IconStar" hugeicons="StarIcon" phosphor="StarIcon" remixicon="RiStarLine" /> Watch </Toggle> <Toggle size="sm" aria-label="Bold"> <IconPlaceholder lucide="BoldIcon" tabler="IconBold" hugeicons="TextBoldIcon" phosphor="TextBIcon" remixicon="RiBold" /> </Toggle> <Toggle size="lg" aria-label="Bold"> <IconPlaceholder lucide="BoldIcon" tabler="IconBold" hugeicons="TextBoldIcon" phosphor="TextBIcon" remixicon="RiBold" /> </Toggle> <Toggle disabled aria-label="Bold"> <IconPlaceholder lucide="BoldIcon" tabler="IconBold" hugeicons="TextBoldIcon" phosphor="TextBIcon" remixicon="RiBold" /> </Toggle> </div> );}<script setup lang="ts">import { Bold, Italic, StarIcon } from "@lucide/vue";import { Toggle } from "@/components/ui/toggle";</script>
<template> <div class="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"><Bold /></Toggle> <Toggle aria-label="Bold" default-value><Bold /></Toggle> <Toggle variant="outline" aria-label="Italic"><Italic /></Toggle> <Toggle variant="outline"> <StarIcon /> Watch </Toggle> <Toggle size="sm" aria-label="Bold"><Bold /></Toggle> <Toggle size="lg" aria-label="Bold"><Bold /></Toggle> <Toggle disabled aria-label="Bold"><Bold /></Toggle> </div></template><script lang="ts"> import { Toggle } from "$lib/components/ui/toggle";
let watching = $state(false);</script>
<div class="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"><b>B</b></Toggle> <Toggle aria-label="Bold" pressed><b>B</b></Toggle> <Toggle variant="outline" aria-label="Italic"><i>I</i></Toggle> <Toggle variant="outline" bind:pressed={watching}>{watching ? "Watching" : "Watch"}</Toggle> <Toggle size="sm" aria-label="Bold"><b>B</b></Toggle> <Toggle size="lg" aria-label="Bold"><b>B</b></Toggle> <Toggle disabled aria-label="Bold"><b>B</b></Toggle></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/togglepnpm dlx shadcn@latest add @edmi-ui/toggleyarn dlx shadcn@latest add @edmi-ui/togglebunx shadcn@latest add @edmi-ui/togglePick 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/react class-variance-authorityTerminal window pnpm add @base-ui/react class-variance-authorityTerminal window yarn add @base-ui/react class-variance-authorityTerminal window bun add @base-ui/react class-variance-authorityAdd 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/togglepnpm dlx shadcn-vue@latest add @edmi-ui/toggleyarn dlx shadcn-vue@latest add @edmi-ui/togglebunx shadcn-vue@latest add @edmi-ui/togglePick 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 class-variance-authority @vueuse/coreTerminal window pnpm add reka-ui class-variance-authority @vueuse/coreTerminal window yarn add reka-ui class-variance-authority @vueuse/coreTerminal window bun add reka-ui class-variance-authority @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/toggle.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.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 { Toggle } from "@/components/ui/toggle";
<div className="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"> <TextBIcon /> </Toggle> <Toggle aria-label="Bold" defaultPressed> <TextBIcon /> </Toggle> <Toggle variant="outline" aria-label="Italic"> <TextItalicIcon /> </Toggle> <Toggle variant="outline"> <StarIcon /> Watch </Toggle> <Toggle size="sm" aria-label="Bold"> <TextBIcon /> </Toggle> <Toggle size="lg" aria-label="Bold"> <TextBIcon /> </Toggle> <Toggle disabled aria-label="Bold"> <TextBIcon /> </Toggle></div><script setup lang="ts">import { Toggle } from "@/components/ui/toggle";</script>
<template> <div class="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"><Bold /></Toggle> <Toggle aria-label="Bold" default-value><Bold /></Toggle> <Toggle variant="outline" aria-label="Italic"><Italic /></Toggle> <Toggle variant="outline"> <StarIcon /> Watch </Toggle> <Toggle size="sm" aria-label="Bold"><Bold /></Toggle> <Toggle size="lg" aria-label="Bold"><Bold /></Toggle> <Toggle disabled aria-label="Bold"><Bold /></Toggle> </div></template><script lang="ts"> import { Toggle } from "$lib/components/ui/toggle";</script>
<div class="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold"><b>B</b></Toggle> <Toggle aria-label="Bold" pressed><b>B</b></Toggle> <Toggle variant="outline" aria-label="Italic"><i>I</i></Toggle> <Toggle variant="outline"></Toggle> <Toggle size="sm" aria-label="Bold"><b>B</b></Toggle> <Toggle size="lg" aria-label="Bold"><b>B</b></Toggle> <Toggle disabled aria-label="Bold"><b>B</b></Toggle></div>Component source
Section titled “Component source”"use client";
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ only the ON item of a segmented toggle rises (copied from the v4 recipes)const raisedActive = "data-[pressed]:[background-image:var(--r1-s-face)] data-[pressed]:[background-origin:border-box] data-[pressed]:border-transparent data-[pressed]:shadow-btn-raised-neutral";
const toggleVariants = cva( "group/toggle inline-flex items-center justify-center gap-1.5 rounded-md border border-transparent text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-[background-color,box-shadow,transform] outline-none hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-invalid:border-destructive data-[pressed]:bg-accent data-[pressed]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-transparent", outline: "border-input bg-transparent text-foreground hover:bg-accent", // ✦ Edmi addition: flat item for a ToggleGroup track (DESIGN §4.5) segmented: "bg-transparent", }, // ✦ depth (v4): default toggles show depth only when ON, outline toggles rise as a whole elevation: { flat: "", sunken: "", raised: "data-[pressed]:shadow-pressed", floating: "data-[pressed]:shadow-pressed", }, size: { sm: "h-8 min-w-8 px-2 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", default: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", lg: "h-[42px] min-w-[42px] px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", }, }, compoundVariants: [ { variant: "outline", elevation: "raised", class: "border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral data-[pressed]:bg-none data-[pressed]:bg-accent data-[pressed]:shadow-pressed", }, { variant: "outline", elevation: "floating", class: "border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral data-[pressed]:bg-none data-[pressed]:bg-accent data-[pressed]:shadow-pressed-float", }, { variant: "segmented", class: "h-[30px] min-w-[30px] rounded-[7px] px-3 hover:bg-transparent data-[pressed]:translate-y-0 data-[pressed]:border-border data-[pressed]:bg-tab-active data-[pressed]:text-foreground", }, { variant: "segmented", elevation: ["raised", "floating"], class: raisedActive, }, ], defaultVariants: { variant: "default", size: "default", elevation: "flat", }, },);
function Toggle({ className, variant = "default", size = "default", elevation, ...props}: TogglePrimitive.Props & Omit<VariantProps<typeof toggleVariants>, "elevation"> & { /** ✦ depth: flat 0, raised +1, floating +2 (default toggles only show it when ON). */ elevation?: Elevation; }) { const level = useElevation( elevation, variant === "outline" ? "button-quiet" : "control", ); return ( <TogglePrimitive data-slot="toggle" className={cn( toggleVariants({ variant, size, elevation: level, className }), )} {...props} /> );}
export { Toggle, toggleVariants };<script setup lang="ts">import type { ToggleEmits, ToggleProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import type { ToggleVariants } from '.'import { reactiveOmit } from '@vueuse/core'import { Toggle, useForwardPropsEmits } from 'reka-ui'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'import { toggleVariants } from '.'
const props = withDefaults(defineProps<ToggleProps & { class?: HTMLAttributes['class'] variant?: ToggleVariants['variant'] size?: ToggleVariants['size'] /** ✦ depth: flat 0, raised +1, floating +2 (default toggles only show it when ON) */ elevation?: Elevation}>(), { variant: 'default', size: 'default', elevation: undefined, disabled: false,})
const emits = defineEmits<ToggleEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'size', 'variant', 'elevation')const forwarded = useForwardPropsEmits(delegatedProps, emits)const controlLevel = useElevation(() => props.elevation, 'control')const quietLevel = useElevation(() => props.elevation, 'button-quiet')const level = computed(() => (props.variant === 'outline' ? quietLevel.value : controlLevel.value))</script>
<template> <Toggle v-slot="slotProps" data-slot="toggle" v-bind="forwarded" :class="cn(toggleVariants({ variant, size, elevation: level }), props.class)" > <slot v-bind="slotProps" /> </Toggle></template>import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";
export { default as Toggle } from "./Toggle.vue";
const raisedActive = "data-[state=on]:[background-image:var(--r1-s-face)] data-[state=on]:[background-origin:border-box] data-[state=on]:border-transparent data-[state=on]:shadow-btn-raised-neutral";
export const toggleVariants = cva( "group/toggle inline-flex items-center justify-center gap-1.5 rounded-md border border-transparent text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-[background-color,box-shadow,transform] outline-none hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-invalid:border-destructive data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-transparent", outline: "border-input bg-transparent text-foreground hover:bg-accent", // ✦ Edmi addition: flat item for a ToggleGroup track (DESIGN §4.5) segmented: "bg-transparent", }, // ✦ depth (v4): default toggles show depth only when ON, outline toggles rise as a whole elevation: { flat: "", sunken: "", raised: "data-[state=on]:shadow-pressed", floating: "data-[state=on]:shadow-pressed", }, size: { sm: "h-8 min-w-8 px-2 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", default: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", lg: "h-[42px] min-w-[42px] px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", }, }, compoundVariants: [ { variant: "outline", elevation: "raised", class: "border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral data-[state=on]:bg-none data-[state=on]:bg-accent data-[state=on]:shadow-pressed", }, { variant: "outline", elevation: "floating", class: "border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral data-[state=on]:bg-none data-[state=on]:bg-accent data-[state=on]:shadow-pressed-float", }, { variant: "segmented", class: "h-[30px] min-w-[30px] rounded-[7px] px-3 hover:bg-transparent data-[state=on]:translate-y-0 data-[state=on]:border-border data-[state=on]:bg-tab-active data-[state=on]:text-foreground", }, { variant: "segmented", elevation: ["raised", "floating"], class: raisedActive, }, ], defaultVariants: { variant: "default", size: "default", elevation: "flat", }, },);
export type ToggleVariants = VariantProps<typeof toggleVariants>;<script lang="ts" module> import { type VariantProps, tv } from "tailwind-variants";
// ✦ only the ON item of a segmented toggle rises (copied from the v4 recipes) export const raisedActive = "data-[state=on]:[background-image:var(--r1-s-face)] data-[state=on]:[background-origin:border-box] data-[state=on]:border-transparent data-[state=on]:shadow-btn-raised-neutral";
export const toggleVariants = tv({ base: "group/toggle inline-flex items-center justify-center gap-1.5 rounded-md border border-transparent text-[13.5px] font-medium whitespace-nowrap text-muted-foreground transition-[background-color,box-shadow,transform] outline-none hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-invalid:border-destructive data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", variants: { variant: { default: "bg-transparent", outline: "border-input bg-transparent text-foreground hover:bg-accent", // ✦ Edmi addition: flat item for a ToggleGroup track (DESIGN §4.5) segmented: "bg-transparent", }, size: { sm: "h-8 min-w-8 px-2 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", default: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", lg: "h-[42px] min-w-[42px] px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", }, // ✦ depth (v4): default toggles show depth only when ON, outline toggles rise as a whole elevation: { flat: "", sunken: "", raised: "data-[state=on]:shadow-pressed", floating: "data-[state=on]:shadow-pressed", }, }, compoundVariants: [ { variant: "outline", elevation: "raised", class: "border-transparent [background-image:var(--r1-s-face)] [background-origin:border-box] shadow-btn-raised-neutral data-[state=on]:bg-none data-[state=on]:bg-accent data-[state=on]:shadow-pressed", }, { variant: "outline", elevation: "floating", class: "border-transparent [background-image:var(--fl-s-face)] [background-origin:border-box] shadow-btn-float-neutral data-[state=on]:bg-none data-[state=on]:bg-accent data-[state=on]:shadow-pressed-float", }, { variant: "segmented", class: "h-[30px] min-w-[30px] rounded-[7px] px-3 hover:bg-transparent data-[state=on]:translate-y-0 data-[state=on]:border-border data-[state=on]:bg-tab-active data-[state=on]:text-foreground", }, { variant: "segmented", elevation: ["raised", "floating"], class: raisedActive, }, ], defaultVariants: { variant: "default", size: "default", elevation: "flat", }, });
export type ToggleVariant = VariantProps<typeof toggleVariants>["variant"]; export type ToggleSize = VariantProps<typeof toggleVariants>["size"]; export type ToggleVariants = VariantProps<typeof toggleVariants>;</script>
<script lang="ts"> import { Toggle as TogglePrimitive } from "bits-ui"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), pressed = $bindable(false), class: className, size = "default", variant = "default", elevation = "auto", ...restProps }: TogglePrimitive.RootProps & { variant?: ToggleVariant; size?: ToggleSize; /** ✦ depth: flat 0, raised +1, floating +2 (default toggles only show it when ON). */ elevation?: Elevation; } = $props();
const controlLevel = useElevation(() => elevation, "control"); const quietLevel = useElevation(() => elevation, "button-quiet"); const level = $derived(variant === "outline" ? quietLevel.current : controlLevel.current);</script>
<TogglePrimitive.Root bind:ref bind:pressed data-slot="toggle" class={cn(toggleVariants({ variant, size, elevation: level }), className)} {...restProps}/>import Root from "./toggle.svelte";
export { raisedActive, type ToggleSize, type ToggleVariant, type ToggleVariants, toggleVariants,} from "./toggle.svelte";
export { Root, // Root as Toggle,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. Raised (+1) and floating (+2) apply to outline toggles as a whole; a default toggle only shows depth while it is ON (pressed in). Sunken is not supported.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { Toggle } from "@/components/ui/toggle";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] 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-3"> <Toggle elevation={value} aria-label="Bold" defaultPressed> <b>B</b> </Toggle> <Toggle elevation={value} variant="outline" aria-label="Italic"> <i>I</i> </Toggle> <Toggle elevation={value} variant="outline" aria-label="Underline" defaultPressed > <u>U</u> </Toggle> </div> </div> ))} </div> );}<script setup lang="ts">import { Toggle } from "@/components/ui/toggle";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</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-3"> <Toggle :elevation="level.value" aria-label="Bold" :default-value="true"><b>B</b></Toggle> <Toggle :elevation="level.value" variant="outline" aria-label="Italic"><i>I</i></Toggle> <Toggle :elevation="level.value" variant="outline" aria-label="Underline" :default-value="true"><u>U</u></Toggle> </div> </div> </div></template><script lang="ts"> import { Toggle } from "$lib/components/ui/toggle";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] 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-3"> <Toggle elevation={level.value} aria-label="Bold" pressed><b>B</b></Toggle> <Toggle elevation={level.value} variant="outline" aria-label="Italic"><i>I</i></Toggle> <Toggle elevation={level.value} variant="outline" aria-label="Underline" pressed><u>U</u></Toggle> </div> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
| variant | default | outline |
default |
| size | sm | default | lg |
default |
| pressed / defaultPressed | boolean |
false |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Pressed state: accent fill; with
elevationraised or floating it also sinks with an inner shadow.outlineraised uses the neutral bevel face, also for toggle-group items.