Tailwind CSS Handbook v4.3

Buttons

Accessible button variants, sizes, states and groups built from Tailwind utilities — plus a reusable component API.

Buttons are the most-copied component in any codebase, so it is worth getting the base right once.

Anatomy#

Every solid button needs six things:

  1. Shape — rounded-md px-4 py-2
  2. Type — text-sm font-medium
  3. Colour — background and text with sufficient contrast
  4. Hover — a visible change
  5. Focus — focus-visible:outline-2 focus-visible:outline-offset-2
  6. Disabled — disabled:opacity-50 disabled:cursor-not-allowed

Variants#

Loading preview…
VariantUse for
PrimaryThe single most important action on the screen
SecondaryAlternative actions of equal weight
GhostLow-emphasis actions in dense UI
DestructiveDelete, revoke, cancel subscription
LinkNavigation styled as text

Sizes#

Loading preview…

States#

Loading preview…

With icons#

Loading preview…

Two rules for icon buttons:

  • Decorative icons get aria-hidden="true".
  • Icon-only buttons need an accessible name — sr-only text or aria-label.

Button groups#

Loading preview…

-ml-px collapses the double border between buttons; focus:z-10 brings the focused ring above its neighbours; isolate keeps that z-index contained.

A reusable component#

Button.jsx
const BASE =
  'inline-flex items-center justify-center gap-2 rounded-md font-medium ' +
  'transition focus-visible:outline-2 focus-visible:outline-offset-2 ' +
  'disabled:pointer-events-none disabled:opacity-50'

const VARIANTS = {
  primary: 'bg-indigo-600 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline-indigo-600',
  secondary: 'bg-white text-gray-900 shadow-sm ring-1 ring-gray-300 ring-inset hover:bg-gray-50 focus-visible:outline-indigo-600',
  ghost: 'text-gray-700 hover:bg-gray-100 focus-visible:outline-indigo-600',
  danger: 'bg-red-600 text-white shadow-sm hover:bg-red-500 focus-visible:outline-red-600',
}

const SIZES = {
  sm: 'px-2.5 py-1.5 text-sm',
  md: 'px-4 py-2 text-sm',
  lg: 'px-5 py-2.5 text-base',
}

export function Button({ variant = 'primary', size = 'md', className = '', ...props }) {
  return (
    <button
      className={`${BASE} ${VARIANTS[variant]} ${SIZES[size]} ${className}`}
      {...props}
    />
  )
}

Note the lookup objects hold complete class names, so Tailwind's source detection finds them. Never build them by interpolation.

HTML
<a href="/signup" class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500">
  Get started
</a>

Common mistakes#

  • Removing focus styles. focus:outline-none with no replacement makes the UI keyboard-unusable.
  • <div> as a button. No keyboard support, no role, no focus.
  • Missing type="button" inside a form. The default is submit, so a "Cancel" button submits the form.
  • Icon-only buttons with no label. Screen readers announce nothing.
  • Insufficient contrast on disabled buttons. opacity-50 can push text below 4.5:1 — that is generally accepted for disabled controls, but do not use the same treatment for anything the user must read.
  • Interpolating variant class names. ` bg-${color}-600 ` produces no CSS.