Styleframe Logo
Forms

Toggle

A button-styled two-state control with an on state driven by aria-pressed, solid, outline, and ghost visual styles, light, dark, and neutral surface colors, and three sizes through the recipe system.

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 :hover fill, 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:

src/components/toggle.styleframe.ts
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:

src/components/Toggle.tsx
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

ColorTokenUse Case
light@color.white (surface)Light surfaces, stays light in dark mode
dark@color.gray-900 (surface)Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use 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

SizeFont SizePadding (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. Set aria-pressed="true" / "false" so assistive technology announces "pressed" / "not pressed" — the WAI-ARIA toggle-button pattern. The recipe's &:aria-pressed rule 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 disabled on the button. Set the native disabled attribute 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:

src/components/toggle.styleframe.ts
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:

src/components/toggle.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

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:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles
options.variantsVariantsCustom variant definitions
options.defaultVariantsRecord<keyof Variants, string>Default variant values
options.compoundVariantsCompoundVariant[]Custom compound variant definitions
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, ghostsolid
sizesm, md, lgmd

Learn more about recipes →

Best Practices

  • Render a real button: Use <button type="button"> with aria-pressed so the control is keyboard-accessible and screen readers announce its on/off state.
  • Use neutral for 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