Toggle
Two-state press-button (`aria-pressed`) built on Radix Toggle, for stateful actions like text formatting and view modes.
Overview
Toggle is built on @radix-ui/react-toggle. A two-state press-button (aria-pressed) for stateful actions outside of form submission - bold / italic in a text editor, view-mode switches, filter chips.
For form-bound boolean values use Checkbox or Switch. Don't use Toggle as navigation - that's Tab or Link. ToggleGroup is the coordinated multi-button equivalent when you have several related toggles.
Preview
'use client';import { Toggle } from '@gremorie/rx-forms';import { Bold, Italic, Underline } from 'lucide-react';export function TogglePreview() { return ( <div className="flex gap-2"> <Toggle aria-label="Bold"> <Bold className="size-4" /> </Toggle> <Toggle aria-label="Italic" defaultPressed> <Italic className="size-4" /> </Toggle> <Toggle aria-label="Underline"> <Underline className="size-4" /> </Toggle> </div> );}Anatomy
Toggle single two-state button (aria-pressed) styled by toggleVariants; data-[state=on] paints the active lookInstallation
bash npx gremorie@latest add rx-toggle bash pnpm dlx gremorie@latest add rx-toggle bash yarn dlx gremorie@latest add rx-toggle bash bunx --bun gremorie@latest add rx-toggle Usage
import { Toggle } from "@gremorie/rx-forms";
import { Bold } from "lucide-react";
export function Example() {
return (
<Toggle aria-label="Bold">
<Bold />
</Toggle>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Toggle>
| Prop | Type | Default | Description |
|---|---|---|---|
pressed | boolean | - | Controlled pressed state. |
defaultPressed | boolean | - | Uncontrolled initial state. |
onPressedChange | (pressed: boolean) => void | - | Fires when the user toggles. |
disabled | boolean | false | Disables interaction. |
variant | "default" | "outline" | "default" | default is transparent; outline adds a border for standalone toolbar use. |
size | "default" | "sm" | "lg" | "default" | Footprint preset. |
Forwards to TogglePrimitive.Root. The pressed state is mirrored via data-state (on / off), which drives the accent background.
toggleVariants
Exported CVA factory. Reused by ToggleGroupItem so children inherit the same surface via context.
import { toggleVariants } from '@gremorie/rx-forms';
<div className={toggleVariants({ variant: 'outline', size: 'sm' })}>
Custom host
</div>;Composition
<Toggle>is a leaf. Always providearia-labelfor icon-only toggles.- Children are typically icons (formatting, view mode). For text + icon, follow the same pattern as
Button. - Use
<ToggleGroup>when you have 2-5 related toggles that should behave like a coordinated set.
Variations
Icon-only formatting toggle
The canonical text-editor pattern. Always pair with aria-label.
import { Bold } from 'lucide-react';
<Toggle aria-label="Bold">
<Bold />
</Toggle>;Default pressed
For toggles whose default state should be "on" (e.g. autosave indicator, an applied filter).
import { Italic } from 'lucide-react';
<Toggle aria-label="Italic" defaultPressed>
<Italic />
</Toggle>;Outline variant
When the toggle stands alone (not inside a ToggleGroup), the outline variant adds a visible boundary so it reads as an interactive control on flat surfaces.
'use client';import { Toggle } from '@gremorie/rx-forms';import { Underline } from 'lucide-react';export function ToggleOutlinePreview() { return ( <Toggle aria-label="Underline" variant="outline"> <Underline /> </Toggle> );}Sizes
Three footprint presets - sm, default, lg - keep toggles aligned with neighbouring controls in a toolbar.
'use client';import { Toggle } from '@gremorie/rx-forms';import { Bold } from 'lucide-react';export function ToggleSizesPreview() { return ( <div className="flex flex-wrap items-center gap-3"> <Toggle aria-label="Bold" size="sm"> <Bold /> </Toggle> <Toggle aria-label="Bold" size="default"> <Bold /> </Toggle> <Toggle aria-label="Bold" size="lg"> <Bold /> </Toggle> </div> );}Disabled
disabled removes the toggle from the tab order and drops its opacity. The pressed state is still visible so users can see what was active.
'use client';import { Toggle } from '@gremorie/rx-forms';import { Italic } from 'lucide-react';export function ToggleDisabledPreview() { return ( <Toggle aria-label="Italic" disabled defaultPressed> <Italic /> </Toggle> );}Controlled with effect
Bind the pressed state to your editor / view state.
function BoldToggle({ editor }) {
const isBold = editor.isActive('bold');
return (
<Toggle
aria-label="Bold"
pressed={isBold}
onPressedChange={(pressed) => editor.chain().focus().toggleBold().run()}
>
<Bold />
</Toggle>
);
}Accessibility
- ARIA: Radix renders
role="button"witharia-pressedreflecting state (true/false). - Keyboard:
SpaceandEntertoggle the button;Tab/Shift+Tabmove focus. - Focus: 3px focus-visible ring driven by
focus-visible:ring-ring/50. - Icon-only: always provide
aria-label. Without it, screen readers announce the toggle with no name. - Distinction from Switch / Checkbox: Toggle is for stateful actions (apply a format, change a view). Switch and Checkbox are for stateful values (a setting, a form field). The visual treatment differs because the mental model differs.
Related
- Toggle Group - coordinated cluster of toggles
- Button - stateless single-action sibling
- Switch - boolean setting (immediate effect)
- Checkbox - boolean form value (on submit)