Toggle
Overview
The Toggle is a two-state button — pressed or not pressed — the kind you reach for in a text-editor toolbar (Bold, Italic) or a view switcher. Semantically it behaves like a checkbox (on/off, and several can be on inside a group), but it is presented as a button. The useToggleRecipe() composable creates a fully configured recipe with color, variant, and size options — plus compound variants that handle every color-variant combination automatically.
The on state rides on aria-pressed: render the control as a native <button>, flip aria-pressed in your handler, and the recipe's &:aria-pressed rule styles the result. There is no pressed recipe argument — it is runtime DOM state, exactly as :hover is not an argument.
The Toggle recipe integrates directly with the default design tokens preset and generates type-safe utility classes at build time with zero runtime CSS.
Why use the Toggle recipe?
The Toggle recipe helps you:
- Ship faster with sensible defaults: Get 3 surface colors, 3 visual styles, and 3 sizes out of the box with a single composable call.
- Use a standards-based on state: The pressed look is driven by
aria-pressed(the WAI-ARIA toggle-button pattern), so the control stays keyboard-accessible with no extra wiring beyond toggling the attribute. - Maintain consistency: The on appearance reuses each color/variant's
:hoverfill, so an on toggle reads as the same gentle hover step everywhere. - Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipe
Add the Toggle recipe to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipe itself:
import { styleframe } from 'virtual:styleframe';
import { useToggleRecipe } from '@styleframe/theme';
const s = styleframe();
const toggle = useToggleRecipe(s);
export default s;
Build the component
Render a native <button>, bind the toggle runtime function to its class, and flip aria-pressed on click. The recipe's &:aria-pressed rule styles the pressed state:
import { useState } from "react";
import { toggle } from "virtual:styleframe";
interface ToggleProps {
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
}
export function Toggle({
color = "neutral",
variant = "solid",
size = "md",
children,
}: ToggleProps) {
const [pressed, setPressed] = useState(false);
return (
<button
type="button"
className={toggle({ color, variant, size })}
aria-pressed={pressed}
onClick={() => setPressed(!pressed)}
>
{children}
</button>
);
}
See it in action
Colors
The Toggle includes 3 color variants: light, dark, and neutral. Like the Checkbox and Switch recipes, these are neutral-spectrum surface colors rather than status colors — the color sets the toggle's surface and text, matching the Button recipe. The solid variant is a filled surface with a subtle border; outline and ghost stay transparent and fill on hover, focus, and when pressed.
The neutral color adapts automatically: dark text on a light surface in light mode, and light text on a dark surface in dark mode, making it the safest default for general-purpose toolbars.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.white (surface) | Light surfaces, stays light in dark mode |
dark | @color.gray-900 (surface) | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default toggle color. It adapts to the user's color scheme automatically, so you don't need to manage light and dark surfaces separately.Variants
The Toggle ships 3 visual styles that mirror the Button recipe's solid, outline, and ghost variants. Each gives hover, focus, and active feedback, and its on state reuses the hover fill; the difference is the resting surface.
Solid
The default. A filled surface with a subtle border — identical to a solid Button at rest — so it reads as clearly clickable. It shifts to a soft gray on hover and focus — the same fill it carries when on. The safe choice for standalone toggles.
Outline
Transparent with a border, so each toggle reads as a distinct slot and fills only when engaged — the natural choice for segmented controls.
Ghost
Transparent with no border, filling only on hover and when pressed — ideal for dense toolbars where borders and a resting fill would add noise.
Sizes
Three size variants from sm to lg control the font size and padding. The border radius stays @border-radius.md at every size.
Size Reference
| Size | Font Size | Padding (Y × X) |
|---|---|---|
sm | @font-size.xs | @0.375 × @0.625 |
md | @font-size.sm | @0.5 × @0.75 |
lg | @font-size.md | @0.625 × @0.875 |
States
On
&:aria-pressed reuses each color/variant's :hover fill (e.g. gray-100) — the same soft surface the toggle shows on hover — so an on toggle reads as gently filled and held. Set aria-pressed="true" on the button (and flip it in your click handler); bind a controlled value or v-model as usual.
Disabled
:disabled dims the toggle to 0.75 opacity, switches the cursor to not-allowed, and removes pointer events. Mirror it with the native disabled attribute so the button also leaves the tab order.
Accessibility
- Render a native
<button>. A real<button type="button">is focusable and activates on Space/Enter for free; the recipe only supplies the styling. - Expose the on state with
aria-pressed. Setaria-pressed="true"/"false"so assistive technology announces "pressed" / "not pressed" — the WAI-ARIA toggle-button pattern. The recipe's&:aria-pressedrule styles the on state. - Give every toggle a name. Visible text content names the control; for an icon-only toggle, add an
aria-label. - Don't rely on the fill alone. The pressed background is the primary visual cue, satisfying it for most cases — but for critical toggles reinforce the state (e.g. a filled vs. outline icon) so it does not rest on a subtle background change. See WCAG 1.4.1.
- Mirror
disabledon the button. Set the nativedisabledattribute in addition to the styling so the control leaves the tab order. - Verify contrast. The focus ring is
@color.primary. Default tokens meet WCAG AA; if you override colors, verify with the WebAIM Contrast Checker.
Customization
Overriding Defaults
The useToggleRecipe() composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import { useToggleRecipe } from '@styleframe/theme';
const s = styleframe();
const toggle = useToggleRecipe(s, {
defaultVariants: {
color: 'neutral',
variant: 'outline',
size: 'lg',
},
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useToggleRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate the neutral color with the outline variant
const toggle = useToggleRecipe(s, {
filter: {
color: ['neutral'],
variant: ['outline'],
},
});
export default s;
API Reference
useToggleRecipe(s, options?)
Creates the toggle recipe — a <button> with color, variant, and size axes whose pressed state (&:aria-pressed) reuses each combination's :hover fill.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles |
options.variants | Variants | Custom variant definitions |
options.defaultVariants | Record<keyof Variants, string> | Default variant values |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, ghost | solid |
size | sm, md, lg | md |
Best Practices
- Render a real button: Use
<button type="button">witharia-pressedso the control is keyboard-accessible and screen readers announce its on/off state. - Use
neutralfor general use: The neutral color adapts to light and dark mode automatically, making it the safest default. - Reach for a toggle, not a checkbox, for instant on/off: Use a toggle for toolbar actions and formatting that apply immediately; use a Checkbox when the choice is submitted with a form.
- Group related toggles: Lay out a set with the Toggle Group recipe, or join them into a connected segmented control with Field Group.
- Reinforce critical states: When the pressed state matters, pair it with an icon change so it doesn't rely on the background fill alone.
FAQ
&:aria-pressed rule (which compiles to &[aria-pressed="true"]) that reuses each color/variant's :hover fill. You toggle the attribute on the <button> in your handler; the CSS lights up automatically. There is no on-color recipe argument — the on look is the hover step of whatever color you picked.
Learn more about compound variants →solid (filled) for standalone toggles that should look clickable, outline (bordered) for segmented controls, and ghost (transparent) for dense toolbars. If you need more surfaces, add them through the options.variants and options.compoundVariants overrides.outline toggles reads as one joined control. For spaced (non-joined) layouts, use the Toggle Group recipe instead.Textarea
A multi-line text-field component with a wrapper-owned visual surface, inline prefix/suffix addons, a resize axis, and invalid/disabled/readonly states. Supports light, dark, and neutral colors, default/soft/ghost styles, and three sizes through the recipe system.
Toggle Group
A layout component for arranging a set of toggles with shared orientation and spacing. Supports horizontal and vertical layouts and three gap sizes through the recipe system.