Skip to content

Toggle

A button that stays pressed or unpressed: bold, favorite, mute. Sizes match Button. For several related toggles, use Toggle Group; for on/off settings in a form, use Switch.

Installation

Add Toggle to your project with the CK-UI CLI. It copies the source into your project, so the code is yours to change.

Terminal
npx ck-ui-cli@latest add toggle
Adds
Toggle.tsxto your CK-UI folder (src/components/ck-ui by default)
Installs
@radix-ui/react-toggleclass-variance-authorityif your project does not have them yet

First time with CK-UI in this project? Run npx ck-ui-cli@latest init once before adding components. The installation guide explains what it sets up.

Manual installationInstall the packages and copy the files yourself.

Install the following dependencies

Run these commands in your terminal to install the required packages

Terminal
npm install @radix-ui/react-toggle class-variance-authority

Create or update your Toggle component

Copy this code to create your Toggle component

/components/ui/Toggle.tsx
"use client";

import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

/*
 * A two-state button (aria-pressed). Built on Radix Toggle.
 * Sizes match Button. Pressed adds a strong border (≥3:1 against the surface and the fill), a fill and the
 * foreground text colour; icon toggles should still swap their icon (e.g. outline → filled star) for important states.
 */

export const toggleVariants = cva(
    [
        "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-lg border size-label-sm",
        "transition-colors duration-base ease-standard focus-ring",
        "text-muted-foreground hover:bg-foreground/5 hover:text-foreground",
        "data-[state=on]:border-foreground/60 data-[state=on]:bg-foreground/10 data-[state=on]:text-foreground",
        "disabled:pointer-events-none disabled:opacity-50",
        "[&_svg]:shrink-0",
    ],
    {
        variants: {
            variant: {
                ghost: "border-transparent bg-transparent",
                stroke: "border-border bg-transparent",
            },
            size: {
                xs: "h-7 min-w-7 px-1.5 [&_svg]:size-4",
                sm: "h-8 min-w-8 px-2 [&_svg]:size-4",
                md: "h-9 min-w-9 px-2.5 [&_svg]:size-5",
                lg: "h-10 min-w-10 px-3 [&_svg]:size-5",
            },
        },
        defaultVariants: { variant: "ghost", size: "md" },
    }
);

export type ToggleVariant = NonNullable<VariantProps<typeof toggleVariants>["variant"]>;
export type ToggleSize = NonNullable<VariantProps<typeof toggleVariants>["size"]>;

export interface ToggleRootProps extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> {
    variant?: ToggleVariant;
    size?: ToggleSize;
}

/** Icon-only toggles need an `aria-label` that names the action (e.g. "Bold"); it must not change with the state. */
const Root = React.forwardRef<React.ElementRef<typeof TogglePrimitive.Root>, ToggleRootProps>(({ className, variant, size, ...props }, ref) => (
    <TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size }), className)} {...props} />
));
Root.displayName = "Toggle.Root";

export { Root };

Test Your Component

  • 1. Add the component to a test page
  • 2. Check all props and variants work as expected
  • 3. Verify responsive behavior
  • 4. Test with different content/data
  • 5. Ensure accessibility features work correctly

Favorite

Controlled with pressed + onPressedChange. Swap the icon (outline → filled) so the state is visible without relying on the background.

Mute

An icon-only toggle with a fixed accessible name.

Sound is on

Sizes and States

xs, sm, md and lg match Button heights; disabled toggles cannot be pressed.

Accessibility

  • A <button> with aria-pressed. Space and Enter toggle it.
  • Icon-only toggles need an aria-label that names the action ("Bold"). Do not change the label with the state; assistive technology already reports pressed / not pressed.
  • The pressed state adds a strong border, a background and the foreground text color, so it does not rely on color alone; also swap the icon for important states.
  • Use Switch for settings that take effect immediately in a form, and Checkbox for values submitted with a form.

API Reference

Built on Radix Toggle. Import with import * as Toggle from "@/components/ui/Toggle".

Toggle.Root

See Radix UI docs for base props.

PropTypeDefaultDescription
pressed"boolean""undefined"

Pressed state (controlled).

defaultPressed"boolean""false"

Initial state (uncontrolled).

onPressedChange"(pressed: boolean) => void""undefined"

Called when toggled.

variant
"ghost""stroke"
"ghost"

Treatment.

size
"xs""sm""md""lg"
"md"

Height, matching Button.

disabled"boolean""false"

Prevents toggling.

asChild"boolean""false"

Render the single child element instead, merging this part's props and behaviour onto it.

className"string""undefined"

Additional classes, merged last.