Skip to content

Tailwind Buttons

Buttons are one of the most frequently reused components in any interface. This lesson shows how to build a consistent button system in Tailwind CSS, covering variants, sizes, states, and accessibility.

What Makes a Good Tailwind Button System?

A solid button system defines a small set of variants (primary, secondary, outline, danger) and sizes (small, medium, large) built from consistent utility patterns, so every button in an app feels part of the same design language.

Buttons should always be real <button> elements (or <a> for navigation), never a styled <div> with a click handler, to keep them keyboard-accessible and properly announced by screen readers.

<button class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2">
  Primary Button
</button>

Background, hover, and focus states are all defined together, forming one complete, reusable button style.

Button Class Structure

class="{base} {color} {hover} {focus} {disabled}"
  • Base layout: padding, radius, font weight, transition.
  • Color: background and text color defining the variant.
  • Hover: a slightly darker or lighter shade for interactive feedback.
  • Focus: an accessible ring using focus-visible:.
  • Disabled: reduced opacity and cursor-not-allowed.

Button Variants Cheatsheet

Common button variant classes for a consistent design system.

Variant Example Classes Use Case
Primary bg-blue-600 hover:bg-blue-700 text-white Main call-to-action
Secondary bg-slate-100 hover:bg-slate-200 text-slate-900 Secondary actions
Outline border border-slate-300 hover:bg-slate-50 Lower-emphasis actions
Danger bg-red-600 hover:bg-red-700 text-white Destructive actions like delete
Ghost hover:bg-slate-100 text-slate-700 Minimal, low-emphasis buttons
Small size px-3 py-1.5 text-sm Compact button size
Large size px-6 py-3 text-lg Prominent, high-visibility button
Icon button p-2 rounded-full Square button containing only an icon
Disabled disabled:opacity-50 disabled:cursor-not-allowed Non-interactive state
Full width w-full Mobile-friendly, full-width action button

Building a Button Variant System

Rather than writing a full class list on every button, extract a reusable component (in React, Vue, or Astro) that accepts a variant prop and maps it to the correct class combination internally.

// Button.astro
---
const { variant = "primary" } = Astro.props;
const variants = {
  primary: "bg-blue-600 hover:bg-blue-700 text-white",
  secondary: "bg-slate-100 hover:bg-slate-200 text-slate-900",
  outline: "border border-slate-300 hover:bg-slate-50 text-slate-700",
};
---
<button class={`rounded-lg px-4 py-2 font-semibold transition-colors ${variants[variant]}`}>
  <slot />
</button>

Every usage of <Button variant="outline"> automatically gets the correct, consistent class combination.

Building Icon Buttons

Icon-only buttons need equal padding on all sides (often with rounded-full) and, critically, an aria-label since there's no visible text for screen readers to announce.

<button aria-label="Close dialog" class="rounded-full p-2 text-slate-500 hover:bg-slate-100 hover:text-slate-700">
  <svg class="h-5 w-5" ...></svg>
</button>

Loading Button State

While an action is processing, disable the button and show a spinner in place of, or alongside, its label so users understand the click registered and shouldn't be repeated.

<button disabled class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-white disabled:opacity-75">
  <svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24">...</svg>
  Saving...
</button>

Writing Meaningful Button Labels

Button text should describe the action it performs. Vague labels like "Click Here" or "Submit" provide little context for users scanning a page or relying on assistive technology.

Good Example

<button class="rounded-lg bg-blue-600 px-4 py-2 text-white">Create Account</button>

Bad Example

<button class="rounded-lg bg-blue-600 px-4 py-2 text-white">Click Here</button>
  • Use specific action verbs: "Create Account", "Download Report", "Add to Cart".
  • Avoid vague labels like "Submit" or "Click Here" whenever a more descriptive option exists.
  • Keep icon-only buttons paired with a descriptive aria-label.

Accessible Buttons

Always use a real <button> element for actions and an <a> for navigation. Styling a <div> to look like a button requires manually re-implementing keyboard and screen reader behavior the browser already provides for free.

  • Use <button> for actions and <a href> for navigation, never a clickable <div>.
  • Add aria-label to any icon-only button without visible text.
  • Ensure disabled buttons use the real disabled attribute, not just visual styling.

Common Mistakes

  • Styling a <div> or <span> to look like a button instead of using a real <button> element.
  • Using vague button text like "Click Here" or "Submit" instead of a specific action description.
  • Forgetting aria-label on icon-only buttons, leaving them unannounced for screen reader users.
  • Not adding a visible focus ring, making keyboard navigation confusing.
  • Repeating the full utility class list on every button instead of extracting a reusable component or variant map.

Key Takeaways

  • A consistent button system defines a small set of variants and sizes built from repeatable utility patterns.
  • Buttons should always be real <button> (or <a> for links) elements, never styled generic containers.
  • Icon-only buttons require an aria-label since there's no visible text to announce.
  • Loading and disabled states should be clearly communicated both visually and through the disabled attribute.
  • Extracting a reusable Button component keeps class lists from being repeated across an entire application.

Pro Tip

Define your button variants once, in a component or a small config object, rather than copy-pasting long utility class lists everywhere; it makes global button style updates a one-line change.