Button
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:
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:
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>
);
}
<script setup lang="ts">
import { button } from "virtual:styleframe";
const {
color = "primary",
variant = "solid",
size = "md",
disabled = false,
} = defineProps<{
color?: "primary" | "secondary" | "success" | "info" | "warning" | "danger";
variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link";
size?: "xs" | "sm" | "md" | "lg" | "xl";
disabled?: boolean;
}>();
</script>
<template>
<button :class="button({ color, variant, size })" :disabled="disabled">
<slot />
</button>
</template>
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
| Color | Token | Use Case |
|---|---|---|
primary | @color.primary | Default actions, links, key information |
secondary | @color.secondary | Secondary actions, neutral states |
success | @color.success | Positive actions, confirmations, completions |
info | @color.info | Informational actions, tips, highlights |
warning | @color.warning | Caution actions, pending states |
danger | @color.danger | Destructive actions, error states, alerts |
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.
Link
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
| Size | Font Size | Border 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>
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:
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:
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;
API Reference
useButtonRecipe(s, options?)
Creates a full button recipe with all variants and compound variant styling.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the button |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | primary, secondary, success, info, warning, danger | primary |
variant | solid, outline, soft, subtle, ghost, link | solid |
size | xs, sm, md, lg, xl | md |
Best Practices
- Choose colors by meaning, not appearance: Use
successfor positive actions anddangerfor destructive actions — this keeps your UI consistent when tokens change. - Establish a clear visual hierarchy: Use
solidfor the primary action,outlineorsoftfor secondary actions, andghostorlinkfor 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
ghostfor 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
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common variant combination as
defaultVariantsso component consumers write less code.
FAQ
The Button recipe uses compound variants to map each color-variant combination to specific styles. For example, when color is primary and variant is solid, the compound variant applies background: @color.primary, color: @color.white, and borderColor: @color.primary-shade-50, along with hover, focus, active, and dark mode overrides. This approach keeps the individual color and variant definitions clean while handling all 36 combinations (6 colors x 6 variants) automatically.
Yes. Override the variants.color and compoundVariants options to add new colors. You'll need to define the empty color variant and add compound variants for each visual style:
const button = useButtonRecipe(s, {
variants: {
color: {
brand: {},
},
},
compoundVariants: [
{ match: { color: 'brand', variant: 'solid' }, css: { background: '@color.brand', color: '@color.white' } },
{ match: { color: 'brand', variant: 'outline' }, css: { color: '@color.brand', borderColor: '@color.brand' } },
// ... soft, subtle, ghost, and link
],
});
ghost and link have a transparent resting state. The difference is that ghost reveals a tinted background on hover (like a toolbar button), while link adds an underline on hover (like a hyperlink). Use ghost for actions within a layout and link for inline navigation actions.subtle also adds a matching colored border, giving the button more visual definition. Use soft when you want a gentler appearance, and subtle when the button needs slightly more structure.filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter variant to only ['solid', 'outline'], all compound variants matching soft, subtle, ghost, or link are excluded from the generated output. Default variants are also adjusted if they reference a removed value.&:disabled pseudo-class in the base configuration. It reduces opacity to 0.75, sets cursor: not-allowed, and disables pointer events. Pass the HTML disabled attribute to your <button> element to activate both the visual style and the accessibility semantics.@color.primary, @font-size.sm, and @border-radius.md through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Badge
A compact labeling component for status indicators, counts, and categorization. Supports multiple colors, visual styles, and sizes through the recipe system.
Overview
Explore Styleframe's utility composables for generating CSS utility classes. Create flexible, reusable styling primitives with full type safety.