Toggle Group
A joined row of toggles that share one value.
import { signal } from "@implementjs/core";
import { BoldIcon, ItalicIcon, UnderlineIcon } from "@implementjs/lucide";
import { ToggleGroup, ToggleGroupItem } from "@/lib/components/ui/toggle-group";
export default function ToggleGroupDemo() {
const value = signal<string[]>(["bold"]);
return ToggleGroup(
{ type: "multiple", value, "aria-label": "Text formatting" },
ToggleGroupItem(
{ value: "bold", variant: "outline", "aria-label": "Toggle bold" },
BoldIcon({ "aria-hidden": true }),
),
ToggleGroupItem(
{ value: "italic", variant: "outline", "aria-label": "Toggle italic" },
ItalicIcon({ "aria-hidden": true }),
),
ToggleGroupItem(
{ value: "underline", variant: "outline", "aria-label": "Toggle underline" },
UnderlineIcon({ "aria-hidden": true }),
),
);
}Installation
npx jsrepo add @implementjs/ui/toggle-group
jsrepo pulls toggle along with it.
Copy the file below to src/lib/components/ui/toggle-group.ts. It imports cn from utils.ts, which belongs at src/lib/utils.ts, and toggle from the same directory — copy those in beside it too.
import { type Child, type ComponentProps } from "@implementjs/core";
import {
ToggleGroup as ToggleGroupPrimitive,
ToggleGroupItem as ToggleGroupItemPrimitive,
} from "@implementjs/primitives";
import { toggleVariants, type ToggleSize, type ToggleVariant } from "./toggle";
import { cn } from "@/lib/utils";
import { createComponent } from "@implementjs/primitives";
export type ToggleGroupProps = ComponentProps<typeof ToggleGroupPrimitive>;
export type ToggleGroupItemProps = ComponentProps<typeof ToggleGroupItemPrimitive> & {
variant?: ToggleVariant;
size?: ToggleSize;
};
export const ToggleGroup = createComponent(function ToggleGroup(
{ class: className, ...props }: ToggleGroupProps,
...children: Child[]
) {
return ToggleGroupPrimitive(
{
...props,
"data-slot": "toggle-group",
class: cn("flex w-fit items-center rounded-md", className),
},
...children,
);
});
export const ToggleGroupItem = createComponent(function ToggleGroupItem(
{ class: className, variant = "default", size = "default", ...props }: ToggleGroupItemProps,
...children: Child[]
) {
return ToggleGroupItemPrimitive(
{
...props,
"data-slot": "toggle-group-item",
"data-variant": variant,
"data-size": size,
class: cn(
toggleVariants({ variant, size }),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10",
"data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className,
),
},
...children,
);
});Usage
The group is styled as a joined row: the items square off against each other and only the ends stay rounded, so an outlined group reads as one control rather than three buttons.
Items take the same variant and size as a standalone toggle — set them per item, since that is where the styles land.
import { signal } from "@implementjs/core";
import { ToggleGroup, ToggleGroupItem } from "@/lib/components/ui/toggle-group";
const value = signal<string[]>(["bold"]);
ToggleGroup(
{ type: "multiple", value, "aria-label": "Text formatting" },
ToggleGroupItem(
{ value: "bold", variant: "outline", "aria-label": "Toggle bold" },
BoldIcon({ "aria-hidden": true }),
),
);
Single or multiple
type defaults to "single" — one item at a time, value a Signal<string | null>. "multiple" lets several stay pressed and makes value a Signal<string[]>.
Outline items
In the outline variant the items drop their left border except on the first, so adjacent borders do not double up into a thick line. That rule keys off data-variant, which the item sets from its own variant prop.
API Reference
Every prop the styling does not consume is forwarded to the Toggle Group primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
ToggleGroup
The root. Owns which items are pressed and the arrow-key focus movement. Sets role="group". Styled as a joined row: items square off against each other and only the ends are rounded. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | Whether pressing an item releases the others, or several can stay on. |
value | Signal<string | null> | Signal<string[]> | — | The pressed value(s). string | null when type is "single", string[] when "multiple". Pass a signal to control it from outside. |
disabled | Signal<boolean> | boolean | false | Disables every item in the group. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
orientation | "horizontal" | "vertical" | "horizontal" | Which arrow keys move focus, and the data-orientation attributes. |
| Data attribute | Value |
|---|---|
[data-toggle-group-root] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
ToggleGroupItem
One toggle. role="radio" with aria-checked in a single group, aria-pressed in a multiple group. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "outline" | "default" | Transparent by default; outline adds a border. Also set as data-variant. |
size | "default" | "sm" | "lg" | "default" | The toggle's height and padding scale. Also set as data-size. |
value* | string | — | Identifies the item. Must be unique within the group. |
disabled | Signal<boolean> | boolean | false | Prevents pressing the item. Sets disabled and data-disabled. |
| Data attribute | Value |
|---|---|
[data-toggle-group-item] | Present |
[data-state] | "on" | "off" |
[data-value] | The item's value |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |