Button
The button pattern provides accessible, semantic buttons with consistent styling and multiple variants. Always set type: type="submit" for the form primary action, type="button" for everything else.
Basic Usage
Section titled “Basic Usage”<button type="button" class="af-btn">Primary Button</button>Variants
Section titled “Variants”Use the af-is-* modifier classes to change the button’s intent. af-btn is primary by default — do not add af-is-primary except with af-is-ghost.
af-is-secondary- Secondary action (less prominent than primary)af-is-tertiary- Tertiary action (least prominent)af-is-success- Success/positive actionaf-is-warning- Warning actionaf-is-danger- Destructive actionaf-is-info- Informational actionaf-is-outline- Transparent background with borderaf-is-on-primary- Invert on a primary-colored surface that is notaf-bg-primaryaf-is-ghost- Transparent, no border (no fill on hover)af-is-link- Text-link: a<button>that looks like a hyperlink (in-page actions, not navigation)
<button type="button" class="af-btn">Primary</button>
<button type="button" class="af-btn af-is-secondary">Secondary</button>
<button type="button" class="af-btn af-is-tertiary">Tertiary</button>
<button type="button" class="af-btn af-is-success">Success</button>
<button type="button" class="af-btn af-is-warning">Warning</button>
<button type="button" class="af-btn af-is-danger">Danger</button>
<button type="button" class="af-btn af-is-info">Info</button>
<button type="button" class="af-btn af-is-outline">Outline</button>Outline
Section titled “Outline”Use af-is-outline to create a border-only button. It can be combined with intent classes.
<button type="button" class="af-btn af-is-outline">Outline</button>
<button type="button" class="af-btn af-is-outline af-is-secondary">Outline Secondary</button>
<button type="button" class="af-btn af-is-outline af-is-tertiary">Outline Tertiary</button>
<button type="button" class="af-btn af-is-outline af-is-success">Outline Success</button>On a primary fill
Section titled “On a primary fill”Inside af-bg-primary, default filled, outline, and ghost invert so they stay readable. Outline hover is a 12% on-primary wash, not a solid fill. Intent variants (af-is-secondary, af-is-danger, and so on) stay themselves.
You do not add a class on the button.
<div class="af-bg-primary af-p-lg af-radius af-inline af-gap-sm">
<button type="button" class="af-btn">Filled</button>
<button type="button" class="af-btn af-is-outline">Outline</button>
<button type="button" class="af-btn af-is-ghost">Ghost</button>
</div>When the parent is a primary-colored surface that is not af-bg-primary (a gradient, or a custom fill), add af-is-on-primary to the button.
<button type="button" class="af-btn af-is-outline af-is-on-primary">Outline</button>Padded transparent button — no border, no fill on hover. Use in toolbars and icon-adjacent actions. Combine with intent classes for colored text.
<button type="button" class="af-btn af-is-ghost">Ghost</button>
<button type="button" class="af-btn af-is-ghost af-is-primary">Ghost primary</button>
<button type="button" class="af-btn af-is-ghost af-is-secondary">Ghost secondary</button>
<button type="button" class="af-btn af-is-ghost af-is-danger">Ghost danger</button>Text-link
Section titled “Text-link”af-is-link is a <button> that looks like a hyperlink: no padding, no min-height, underline. Use it for in-page actions (Show more, Undo), not for navigation.
For navigation that should look like a button, see Link Button. For actual links, use <a>.
<button type="button" class="af-btn af-is-link">Show more</button>
<button type="button" class="af-btn af-is-link">Undo</button>
<button type="button" class="af-btn af-is-link af-is-danger">Delete</button>Buttons come in five sizes:
<button type="button" class="af-btn af-is-xs">XS</button>
<button type="button" class="af-btn af-is-sm">Small</button>
<button type="button" class="af-btn">Default</button>
<button type="button" class="af-btn af-is-lg">Large</button>
<button type="button" class="af-btn af-is-xl">XL</button>Full width
Section titled “Full width”There is no af-is-block, af-is-full, or af-btn-block. Stretch a button with the af-w-full utility. Do not use af-block on buttons: it sets display: block and replaces the button’s inline-flex layout.
<button type="submit" class="af-btn af-w-full">Continue</button>Responsive: af-w-full@md from that breakpoint and up. See Sizing.
Link as button
Section titled “Link as button”For navigation that should look like a button (<a class="af-btn">), see Link Button. Use <button type="button" class="af-btn"> for in-page actions.
Component Tokens
Section titled “Component Tokens”Buttons expose a few component tokens for easy sizing, spacing, and hover overrides:
--af-control-min-height(shared across controls)--af-btn-min-height--af-btn-gap--af-btn-icon-size--af-btn-padding-x,--af-btn-padding-y,--af-btn-radius--af-btn-border-width(defaults to--af-border-width)--af-btn-border,--af-btn-border-hover,--af-btn-border-focus,--af-btn-border-auto--af-btn-default-border,--af-btn-secondary-border,--af-btn-tertiary-border,--af-btn-success-border,--af-btn-warning-border,--af-btn-danger-border,--af-btn-info-border(and matching-hover/-focus)--af-btn-outline-border,--af-btn-outline-hover--af-btn-ghost-border--af-btn-hover-brightness,--af-btn-active-brightness(darken in light theme, lighten in dark)
<style>
:root {
--af-btn-min-height: 3rem;
--af-btn-gap: 0.75rem;
}
</style>
<button type="button" class="af-btn">Primary</button>Corner radius
Section titled “Corner radius”Buttons follow --af-btn-radius (defaults to --af-radius, same as cards and inputs). Set --af-btn-radius to --af-pill-radius for pills, or --af-radius-sm for Tailwind-like rounded-md.
<style>
:root {
--af-btn-radius: var(--af-pill-radius);
}
</style>
<div class="af-inline btn-radius-full-demo">
<button type="button" class="af-btn">Save</button>
<button type="button" class="af-btn af-is-secondary">Cancel</button>
<button type="button" class="af-btn af-is-outline">Outline</button>
</div>Borders
Section titled “Borders”Filled buttons already reserve a 1px border. It is transparent by default, so the default look does not change. Set --af-btn-border to paint every filled variant, or --af-btn-*-border for one variant. Hover and focus follow that rest color unless you set the matching -hover / -focus tokens.
Outline always uses --af-btn-outline-border (see Outline). Ghost and link ignore --af-btn-border. Keyboard focus still uses the global focus ring; --af-btn-border-focus is extra chrome.
Auto border
Section titled “Auto border”Point --af-btn-border at --af-btn-border-auto to turn borders on without picking hexes. Each filled intent remaps that token (primary uses --af-color-primary-active, success uses --af-color-success-active, and so on). Ghost stays borderless.
<style>
:root {
--af-btn-border: var(--af-btn-border-auto);
}
</style>
<div class="af-inline">
<button type="button" class="af-btn">Primary</button>
<button type="button" class="af-btn af-is-secondary">Secondary</button>
<button type="button" class="af-btn af-is-tertiary">Tertiary</button>
<button type="button" class="af-btn af-is-success">Success</button>
<button type="button" class="af-btn af-is-warning">Warning</button>
<button type="button" class="af-btn af-is-danger">Danger</button>
<button type="button" class="af-btn af-is-info">Info</button>
<button type="button" class="af-btn af-is-ghost">Ghost</button>
</div>Same border on every filled button
Section titled “Same border on every filled button”<style>
:root {
--af-btn-border: var(--af-color-border);
}
</style>
<div class="af-inline">
<button type="button" class="af-btn">Primary</button>
<button type="button" class="af-btn af-is-secondary">Secondary</button>
<button type="button" class="af-btn af-is-tertiary">Tertiary</button>
<button type="button" class="af-btn af-is-success">Success</button>
<button type="button" class="af-btn af-is-danger">Danger</button>
<button type="button" class="af-btn af-is-ghost">Ghost</button>
</div>Override specific variants
Section titled “Override specific variants”Override a single filled intent. Other variants stay transparent unless --af-btn-border is also set. Set the token back to transparent to turn that variant off.
<style>
:root {
--af-btn-danger-border: var(--af-color-danger-active);
}
</style>
<div class="af-inline">
<button type="button" class="af-btn">Primary</button>
<button type="button" class="af-btn af-is-secondary">Secondary</button>
<button type="button" class="af-btn af-is-danger">Danger</button>
</div>States
Section titled “States”Hover / active
Section titled “Hover / active”Filled buttons darken on hover in light theme and lighten in dark theme (--af-btn-hover-brightness / --af-btn-active-brightness). Outline buttons use a soft fill on hover instead.
Hover the live examples above to see it. Tertiary (dark fill) always lightens.
Disabled
Section titled “Disabled”For accessibility, prefer the native disabled attribute first (for <button> elements), with the CSS class af-is-disabled available as a fallback (e.g., for <a> tags instead of <button>):
<!-- Preferred: Use native disabled attribute for accessibility -->
<button type="button" class="af-btn" disabled>Disabled Button</button>
<button type="button" class="af-btn" disabled>Disabled Primary</button>
<!-- Fallback: CSS class for non-button elements -->
<a href="#" class="af-btn af-is-disabled">Disabled Link</a>Loading
Section titled “Loading”Use the loading state to indicate an action in progress. Prefer aria-busy="true" — Airframe draws a CSS spinner and hides the label visually while keeping the accessible name. af-is-loading is the class fallback (for <a class="af-btn">).
<button type="button" class="af-btn" aria-busy="true">Saving</button>
<button type="button" class="af-btn af-is-ghost" aria-busy="true">Loading</button>
<a href="#" class="af-btn af-is-loading">Redirecting…</a>Active
Section titled “Active”For toggle buttons or active states. For accessibility, prefer ARIA attributes first (aria-pressed="true" for toggles or aria-current="page" for navigation), with CSS classes available as a fallback (e.g., for <a> tags instead of <button>):
<!-- Preferred: Use ARIA attribute for accessibility -->
<button type="button" class="af-btn" aria-pressed="true">Active</button>
<a href="#" class="af-btn" aria-current="page">Current Page</a>
<!-- Fallback: CSS class for non-button elements -->
<a href="#" class="af-btn af-is-active">Active</a>Accessibility
Section titled “Accessibility”- Uses native
<button>element for proper semantics - Visible focus ring enabled by default
- Keyboard accessible (Enter/Space)
- Screen reader friendly
- Icon buttons must have
aria-labelfor accessibility
Best Practices
Section titled “Best Practices”- Always use semantic
<button>elements, not<div>with click handlers - Visible focus is enabled by default and should not be removed
- Disabled buttons maintain proper ARIA attributes
- Use link buttons (
<a class="af-btn">) only for navigation, not actions - Provide
aria-labelfor icon-only buttons - Stretch a button with
af-w-full. Do not inventaf-is-block,af-is-full, oraf-btn-block