Skip to content

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.

<button type="button" class="af-btn">Primary Button</button>

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 action
  • af-is-warning - Warning action
  • af-is-danger - Destructive action
  • af-is-info - Informational action
  • af-is-outline - Transparent background with border
  • af-is-on-primary - Invert on a primary-colored surface that is not af-bg-primary
  • af-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>

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>

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>

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>

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.

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.

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>

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>

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.

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>
<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 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>

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.

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>):

Disabled Link
<!-- 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>

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">).

Redirecting…
<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>

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>
  • 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-label for accessibility
  • 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-label for icon-only buttons
  • Stretch a button with af-w-full. Do not invent af-is-block, af-is-full, or af-btn-block