Skip to content

Toggle Group

A set of toggles that behaves like a toolbar: several can be on at once (type="multiple") or at most one (type="single"). The group is one tab stop; arrow keys move between items.

Installation

Add Toggle Group 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-group
Adds
ToggleGroup.tsxToggle.tsxto your CK-UI folder (src/components/ck-ui by default)
Installs
@radix-ui/react-toggle@radix-ui/react-toggle-groupclass-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-group @radix-ui/react-toggle class-variance-authority

Add the Toggle component

ToggleGroup imports ./Toggle, so add this file next to it

/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 };

Create or update your ToggleGroup component

Copy this code to create your ToggleGroup component

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

import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { cn } from "@/lib/utils";
import { toggleVariants, type ToggleSize, type ToggleVariant } from "./Toggle";

/*
 * A set of toggles: type="single" (at most one pressed) or type="multiple" (independent toggles), built on
 * Radix ToggleGroup. One tab stop for the group; arrow keys move between items (roving focus).
 * Use it for toolbar-style formatting/filters. For choosing one view or mode, use SegmentedControl.
 */

const ToggleGroupContext = React.createContext<{ variant?: ToggleVariant; size?: ToggleSize }>({});

export type ToggleGroupRootProps = React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & {
    variant?: ToggleVariant;
    size?: ToggleSize;
};

/** Name the group with `aria-label` (e.g. "Text formatting"). */
const Root = React.forwardRef<React.ElementRef<typeof ToggleGroupPrimitive.Root>, ToggleGroupRootProps>(({ className, variant, size, children, ...props }, ref) => {
    const context = React.useMemo(() => ({ variant, size }), [variant, size]);

    return (
        <ToggleGroupPrimitive.Root
            ref={ref}
            className={cn("inline-flex items-center gap-1 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch", className)}
            {...props}
        >
            <ToggleGroupContext.Provider value={context}>{children}</ToggleGroupContext.Provider>
        </ToggleGroupPrimitive.Root>
    );
});
Root.displayName = "ToggleGroup.Root";

export interface ToggleGroupItemProps extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> {
    variant?: ToggleVariant;
    size?: ToggleSize;
}

const Item = React.forwardRef<React.ElementRef<typeof ToggleGroupPrimitive.Item>, ToggleGroupItemProps>(({ className, variant, size, ...props }, ref) => {
    const context = React.useContext(ToggleGroupContext);
    return <ToggleGroupPrimitive.Item ref={ref} className={cn(toggleVariants({ variant: variant ?? context.variant, size: size ?? context.size }), className)} {...props} />;
});
Item.displayName = "ToggleGroup.Item";

export { Root, Item };

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

Single Selection

type="single" allows at most one pressed item. Pressing it again clears the value; ignore empty values to always keep one.

Alignment: left

Vertical

orientation="vertical" stacks the items; use ArrowUp/ArrowDown.

Toggle Group vs Segmented Control

  • Toggle Group: toolbar-style toggles that change something (formatting, filters). Items can be independent (multiple) or exclusive (single), and the group can end up with nothing pressed.
  • Segmented Control: choose exactly one mode or view (List / Board / Calendar). One option is always selected.

Accessibility

  • The group has role="group"; name it with aria-label.
  • Multiple: each item is a button with aria-pressed. Single: items are exposed as radio buttons with aria-checked.
  • One tab stop for the whole group (roving focus): arrow keys move between items, Home/End jump to the ends; disabled items are skipped.
  • Icon-only items need an aria-label; the label does not change with the state.

API Reference

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

ToggleGroup.Root

See Radix UI docs for base props.

PropTypeDefaultDescription
type
"single""multiple"
"-"

Required. Exclusive or independent items.

value / defaultValue"string | string[]""undefined"

Pressed value(s); a string for single, an array for multiple.

onValueChange"(value) => void""undefined"

Called with the new value(s).

orientation
"horizontal""vertical"
"horizontal"

Layout and arrow-key direction.

rovingFocus"boolean""true"

One tab stop with arrow-key navigation.

loop"boolean""true"

Arrow keys wrap around.

disabled"boolean""false"

Disables every item.

variant
"ghost""stroke"
"ghost"

Passed to every item.

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

Passed to every item.

className"string""undefined"

Additional classes, merged last.

ToggleGroup.Item

See Radix UI docs for base props.

PropTypeDefaultDescription
value"string""-"

Required. The item's value.

disabled"boolean""false"

Skipped by the keyboard.

variant / size"-""group's"

Override the group's treatment for one item.

className"string""undefined"

Additional classes, merged last.