Toggle

A two-state button that can be either on or off.

Basic
Outline
Sizes
With Button Text
With Button Icon
With Button Icon + Text
Disabled
With Icon

Installation

Install the dependencies

pnpm add cn class-variance-authority github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/toggle.

src/lib/components/ui/toggle/Toggle.svelte
<script lang="ts">
  import { Toggle as Primitive } from '@sveltery/base/toggle';
  import { cn } from 'cn';
  import type { ToggleProps } from './types.js';
  import { toggleVariants } from './variants.js';
  let { pressed = $bindable(), class: classProp, variant = 'default', size = 'default', ...props }: ToggleProps = $props();
</script>
<Primitive data-slot="toggle" bind:pressed {...props} class={cn(toggleVariants({ variant, size }), classProp)} />
src/lib/components/ui/toggle/types.ts
import type { ToggleProps as BaseToggleProps } from '@sveltery/base/toggle';
import type { ToggleVariants } from './variants.js';
export type ToggleProps = BaseToggleProps & ToggleVariants;
export type { ToggleState } from '@sveltery/base/toggle';
src/lib/components/ui/toggle/variants.ts
// Adapted from shadcn-ui/ui d75a96ab; MIT: packages/ui/THIRD_PARTY_NOTICES.md.
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
export const toggleVariants = cva("hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
  variants: {
    variant: { default: 'bg-transparent', outline: 'border-input hover:bg-muted border bg-transparent' },
    size: { default: 'h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", lg: 'h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2' },
  },
  defaultVariants: { variant: 'default', size: 'default' },
});
export type ToggleVariants = VariantProps<typeof toggleVariants>;
src/lib/components/ui/toggle/index.ts
export { default as Toggle } from './Toggle.svelte';
export { toggleVariants } from './variants.js';
export type { ToggleVariants } from './variants.js';
export type { ToggleProps, ToggleState } from './types.js';

Update the import paths

Change the imports to match your project setup.

Usage

<script lang="ts">
  import { Toggle } from '$lib/components/ui/toggle';
</script>
<Toggle aria-label="Toggle italic">Italic</Toggle>

Pressed state

pressed holds the state. Pass it to start the toggle on, or bind it to read and write it.

<script lang="ts">
  let bold = $state(false);
</script>

<Toggle aria-label="Toggle bold" bind:pressed={bold}>Bold</Toggle>

Outline

<Toggle variant="outline" aria-label="Toggle italic">Italic</Toggle>

Sizes

size is sm, default or lg.

<Toggle size="sm" aria-label="Toggle small">Small</Toggle>
<Toggle size="lg" aria-label="Toggle large">Large</Toggle>

Disabled

<Toggle disabled aria-label="Toggle disabled">Disabled</Toggle>

API reference

Toggle is styled on top of the Sveltery Base Toggle primitive, which owns its pressed state, keyboard behavior and accessibility. toggleVariants returns the class string for variant and size. See the Base UI Toggle documentation for the behavior it ports.