Composables

Button

An interactive control component for actions and navigation. Supports multiple colors, visual styles, sizes, and states through the recipe system.

Overview

The Button is an interactive UI element used for triggering actions, submitting forms, and navigating between views. The useButtonRecipe() composable creates a fully configured recipe with color, variant, and size options — plus compound variants that handle the color-variant combinations automatically.

The Button 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 Button recipe?

The Button recipe helps you:

  • Ship faster with sensible defaults: Get 6 colors, 6 visual styles, and 5 sizes out of the box with a single composable call.
  • Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including hover, focus, active, and dark mode states.
  • 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 Button 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/button.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useButtonRecipe } from '@styleframe/theme';

const s = styleframe();

const button = useButtonRecipe(s);

export default s;

Build the component

Import the button runtime function from the virtual module and pass variant props to compute class names:

src/components/Button.tsx
import { button } from "virtual:styleframe";

interface ButtonProps {
    color?: "primary" | "secondary" | "success" | "info" | "warning" | "danger";
    variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link";
    size?: "xs" | "sm" | "md" | "lg" | "xl";
    disabled?: boolean;
    children?: React.ReactNode;
}

export function Button({
    color = "primary",
    variant = "solid",
    size = "md",
    disabled = false,
    children,
}: ButtonProps) {
    const classes = button({ color, variant, size });

    return (
        <button className={classes} disabled={disabled}>
            {children}
        </button>
    );
}

See it in action

Colors

The Button recipe includes 6 semantic color variants: primary, secondary, success, info, warning, and danger. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including hover, focus, active, and dark mode states.

Color Reference

ColorTokenUse Case
primary@color.primaryDefault actions, links, key information
secondary@color.secondarySecondary actions, neutral states
success@color.successPositive actions, confirmations, completions
info@color.infoInformational actions, tips, highlights
warning@color.warningCaution actions, pending states
danger@color.dangerDestructive actions, error states, alerts
Pro tip: Use semantic color names that describe purpose, not appearance. This makes it easier to update your palette without touching component code.

Variants

Six visual style variants control how the button is rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background, text, and border colors for your chosen color.

Solid

Filled background with light text. The most prominent style, ideal for primary actions and call-to-action buttons.

Outline

Transparent background with colored border and text. Useful for secondary actions that shouldn't dominate the visual hierarchy.

Soft

Light tinted background with colored text. A subtle but visible style that works well for tertiary actions and grouped controls.

Subtle

Light tinted background with colored text and a matching border. Combines the softness of the soft variant with the definition of outline.

Ghost

Transparent background with colored text that reveals a tinted background on hover. Ideal for toolbar actions and icon buttons where the button chrome should be invisible until interaction.

Styled as an inline text link with no background or border. On hover, the text darkens and gains an underline. Use for navigation actions that should look like hyperlinks.

Sizes

Five size variants from xs to xl control the font size, padding, gap, and border radius of the button.

Size Reference

SizeFont SizeBorder Radius
xs@font-size.xs@border-radius.md
sm@font-size.sm@border-radius.md
md@font-size.sm@border-radius.md
lg@font-size.md@border-radius.md
xl@font-size.lg@border-radius.lg

Disabled

The Button recipe includes a built-in disabled state through the &:disabled pseudo-class. Disabled buttons have reduced opacity, a not-allowed cursor, and pointer-events: none to prevent interaction.

Accessibility

Buttons are one of the most critical interactive elements for accessibility. The Button recipe includes built-in support for focus visibility and disabled states.

Use semantic HTML

Always render buttons with the <button> element (or <a> for navigation). This provides built-in keyboard support (Enter and Space to activate) and screen reader announcements without additional ARIA attributes.

<!-- Correct: semantic button element -->
<button class="...">Save Changes</button>

<!-- Correct: link styled as button for navigation -->
<a href="/settings" class="...">Settings</a>

<!-- Avoid: non-semantic element as button -->
<div class="..." onclick="save()">Save Changes</div>

Focus visibility

The recipe includes a :focus-visible style that displays a 2px solid outline in the primary color with a 2px offset. This ring appears only during keyboard navigation, not on mouse clicks, following the WCAG 2.4.7 (Focus Visible) guideline.

Disabled state

The :disabled pseudo-class reduces opacity to 0.75, sets cursor: not-allowed, and applies pointer-events: none to prevent interaction. Always pair the visual disabled state with the HTML disabled attribute so assistive technologies can communicate the state.

<!-- Correct: visual style + HTML attribute -->
<button class="..." disabled>Submit</button>

<!-- Avoid: visual-only disabled state -->
<button class="..." style="opacity: 0.75">Submit</button>
Good practice: When buttons appear next to each other in a toolbar or form, use the variant prop to create visual hierarchy. For example, use solid for the primary action and outline or ghost for secondary actions.

Customization

Overriding Defaults

The useButtonRecipe() 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/button.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useButtonRecipe } from '@styleframe/theme';

const s = styleframe();

const button = useButtonRecipe(s, {
    base: {
        borderRadius: '@border-radius.full',
    },
    defaultVariants: {
        color: 'success',
        variant: 'soft',
        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/button.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useButtonRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate primary and danger colors, with solid and outline styles
const button = useButtonRecipe(s, {
    filter: {
        color: ['primary', 'danger'],
        variant: ['solid', '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

useButtonRecipe(s, options?)

Creates a full button recipe with all variants and compound variant styling.

Parameters:

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

Variants:

VariantOptionsDefault
colorprimary, secondary, success, info, warning, dangerprimary
variantsolid, outline, soft, subtle, ghost, linksolid
sizexs, sm, md, lg, xlmd

Learn more about recipes →

Best Practices

  • Choose colors by meaning, not appearance: Use success for positive actions and danger for destructive actions — this keeps your UI consistent when tokens change.
  • Establish a clear visual hierarchy: Use solid for the primary action, outline or soft for secondary actions, and ghost or link for tertiary actions within the same context.
  • Stick to one or two sizes per context: Mixing too many sizes in the same area creates visual noise. Pick a default size and use alternatives sparingly.
  • Use ghost for toolbars and icon buttons: The transparent resting state keeps the interface clean while the hover state confirms interactivity.
  • Filter what you don't need: If your component only uses a few variants, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.

FAQ