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.
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.
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.
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 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.
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.
You now understand how to build a consistent, accessible button system. Next, learn Tailwind Cards for building content containers.