Skip to content
edmi-ui

Toggle

Two-state button with a pressed state.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/toggle
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/toggle
Terminal window
yarn dlx shadcn@latest add @edmi-ui/toggle
Terminal window
bunx shadcn@latest add @edmi-ui/toggle

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/toggle
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/toggle
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/toggle
Terminal window
bunx shadcn-vue@latest add @edmi-ui/toggle

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/toggle.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/toggle.tsx
"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 };
registry/ui/toggle/Toggle.vue
<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>
registry/ui/toggle/index.ts
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>;
src/lib/registry/ui/toggle/toggle.svelte
<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}
/>
src/lib/registry/ui/toggle/index.ts
import Root from "./toggle.svelte";
export {
raisedActive,
type ToggleSize,
type ToggleVariant,
type ToggleVariants,
toggleVariants,
} from "./toggle.svelte";
export {
Root,
//
Root as Toggle,
};

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

Prop Type Default
variant default | outline default
size sm | default | lg default
pressed / defaultPressed boolean false
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • Pressed state: accent fill; with elevation raised or floating it also sinks with an inner shadow. outline raised uses the neutral bevel face, also for toggle-group items.