Does Tailwind CSS include a button component?
No. Tailwind has no button component, only the native button element and the utilities you put on it.
The Tailwind v1 component examples page said so plainly, and the v4.3 documentation says it through its structure. Its utility groups run from layout and spacing to SVG and accessibility, and none of them is a button.
The reason comes down to how the framework works. Tailwind is a utility-first framework, so each class sets one declaration and the markup composes everything else.
The framework does supply utilities for padding, color, radius, shadow and type, plus state variants like hover, focus-visible and disabled. Preflight, the base reset, strips the browser’s own button styling.
The rest is your job.
- Choosing the element and its type attribute
- Defining variants and sizes
- Focus visibility and contrast
- Reusing the class string across the project
The native element brings real behavior with it. A button takes focus with Tab, activates on Enter and Space, and submits its form unless the type is set to button.
Bootstrap goes the other way, with ready-made button classes that carry their own look. Tailwind gives up the pre-built look in exchange for control over every property. The official editor extension, Tailwind CSS IntelliSense, autocompletes class names as you type, which takes most of the sting out of the long strings.
Which utility classes make a basic Tailwind button?
A basic button needs px-4, py-2, text-sm, font-medium, rounded-md, bg-blue-600 and text-white. That’s the whole recipe.
<button type="button" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white">
Save changes
</button>
You need all of them because Preflight wipes the browser look. Per the Preflight stylesheet in the Tailwind repository, buttons inherit font and color, get a border radius of 0, and start with a transparent background.
Skip the utilities and the button reads as plain text.
- px-4 compiles to padding-inline of calc(var(–spacing) * 4), which is 1rem at the default 0.25rem spacing value
- text-sm and font-medium handle the type
- rounded-md gives the shape
- The background and text color need enough contrast between them
Radius and shadow names moved in v4. Per the Tailwind CSS upgrade guide, rounded became rounded-sm, rounded-sm became rounded-xs, and shadow-sm became shadow-xs, so a copied v3 snippet that uses the -sm names looks different until it is renamed.
Set type=”button” on any button inside a form, since the default type submits the form. And let the Prettier plugin for Tailwind CSS sort classes into a fixed order, so diffs stay readable.
How to build solid, outline, ghost, and destructive button variants
A variant changes only color utilities (background, text, border and the hover shade). Padding, radius and type stay identical across the set.
| Variant | Base utilities | Hover | Typical use |
|---|
| Solid | bg-blue-600 text-white | hover:bg-blue-700 | Primary action |
| Soft | bg-blue-50 text-blue-700 | hover:bg-blue-100 | Secondary action |
| Outline | border border-blue-600 text-blue-600 | hover:bg-blue-50 | Alternative action |
| Ghost | text-blue-600 | hover:bg-blue-50 | Toolbar, low emphasis |
| Destructive | bg-red-600 text-white | hover:bg-red-700 | Delete, remove |
Solid and soft variants
Solid is the default for the main action on a screen. It carries the weight a call-to-action button needs, so use it once per view.
Soft keeps the hue but drops the saturation, which makes it a clear secondary choice next to a solid button without competing with it.
Outline and ghost variants
Outline is a border plus a matching text color, with a light fill on hover. In v4 a bare border follows the text color, because the default border color changed from gray-200 to currentColor (Tailwind CSS upgrade guide), so the separate border color utility can be dropped.
Ghost has no border and no fill until hover. It’s the quietest variant in the set.
Ordered this way, the variants build a visual hierarchy, with solid at the top, ghost at the bottom, and soft and outline in between.
Destructive variant
Destructive is just the solid pattern on the red scale, with the same padding, radius and type as the primary button. Label it with words that name the action, since color alone shouldn’t carry the meaning.
Use soft red for reversible actions and solid red for the final confirm step.
How to style hover, focus, active, and disabled states
Each state gets its own prefixed utility. hover:, focus-visible:, active: and disabled: apply a class only while that condition holds.
<button type="button" class="bg-blue-600 text-white transition-colors hover:bg-blue-700 active:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:cursor-not-allowed disabled:opacity-50">
Save changes
</button>
The hover variant fires with a pointer over the button, on devices that support hover. focus-visible reacts to keyboard focus only, so mouse clicks leave no ring. active is the moment of pressing, and disabled matches the native disabled attribute.
Version 4.0 changed the hover variant. It now applies only when the primary input device supports hover (Tailwind CSS upgrade guide). A tap on a touch screen no longer triggers hover styles, and a project that depended on tap-to-hover restores the old behavior with @custom-variant hover (&:hover);.
I’d go with focus-visible over plain focus for buttons. In Chrome and Firefox, plain focus matches after a mouse click, which leaves a ring behind every press.
The disabled: variant matches only elements with the native disabled attribute. A link styled as a button needs aria-disabled: instead, a variant Tailwind includes for aria-disabled=”true”.
Headless UI’s Button takes a different route and exposes state as data attributes (data-hover, data-focus, data-active, data-disabled), which Tailwind reads with variants like data-hover:bg-sky-500 (Headless UI documentation).
Hover and transition effects
The transition-colors utility runs for 150ms with a cubic-bezier(0.4, 0, 0.2, 1) easing by default (Tailwind CSS docs, v4.3).
That short fade is one of the small micro-interactions that tell a user the button registered the pointer.
In v4, transition and transition-colors include outline-color, so a focus outline with a custom color animates from the default color unless the color is set unconditionally (Tailwind CSS upgrade guide). Reduced-motion users are the other catch, and motion-safe:transition drops the effect under their system setting.
Loading state with animate-spin
A loading button is a disabled button with a spinner inside it. The Tailwind documentation example places an SVG with animate-spin before the label and hides it under reduced motion with motion-reduce:hidden.
- Set the disabled attribute so a second click cannot fire
- Add animate-spin to the SVG
- Change the label to a progress word such as “Processing…”
If you’d rather not build the spinner yourself, a dedicated Tailwind spinner component drops straight into the button.
How to size buttons, add icons, and make them full width
Sizing comes down to a padding and text-size pair. One scale from xs to xl keeps every button in the project consistent.
Size scale
- xs: px-2 py-1 text-xs
- sm: px-3 py-1.5 text-sm
- md: px-4 py-2 text-sm
- lg: px-5 py-2.5 text-base
- xl: px-6 py-3 text-base
Padding comes from the –spacing theme variable (Tailwind CSS docs, v4.3). Changing it rescales every spacing utility in the project, not only buttons.
Icon and icon-only buttons
Put inline-flex and items-center on the button, add gap-2 for spacing, then give the SVG size-4. The size-* utilities set width and height together (Tailwind CSS docs, v4.3).
An icon-only button swaps the horizontal padding for square padding such as p-2 and needs an aria-label, because an SVG alone gives screen readers no name.
Full-width and responsive width
Don’t use sm: to target phones. Put w-full on the button for phones and sm:w-auto for 640px and up.
Tailwind is mobile-first, so unprefixed utilities apply at every width and prefixed ones apply from their breakpoint upward. The sm breakpoint sits at 40rem (640px), per the Tailwind CSS responsive design docs.
For touch screens, the pointer-coarse: variant (for example pointer-coarse:py-3) enlarges the target only on devices with an imprecise pointer.
How to keep a Tailwind button accessible
Tailwind leaves accessibility to the class list. Use a native element for actions, and make sure the focus indicator and label text have enough contrast. The target also needs to be big enough for a thumb.
Button, anchor, or input
Actions use button. Navigation uses a.
An input with type=”submit” is the legacy third option, and MDN Web Docs explains why button wins. It accepts child markup such as an SVG icon, while input only carries a value attribute for its label.
A link styled as a button keeps the a element and its href with the same classes applied, and screen readers still announce a link, which is correct for navigation. The reverse is a button styled as a link. It still announces as a button and fires an action, so reserve link-style classes for actions.
Focus ring and contrast
Deleting the outline without a replacement strands keyboard users. Version 4 also changed what the old class does. outline-none now sets outline-style to none, and the invisible-outline behavior moved to outline-hidden, which still shows in forced colors mode (Tailwind CSS upgrade guide).
Test the focus color against both the button fill and the page behind it, and give the indicator an offset so it separates from the fill. Hover and active fills need their own contrast check as well.
MDN advises leaving the default focus ring alone unless the replacement has enough contrast, and WCAG 1.4.11 sets the bar at 3:1 for an author-styled indicator (W3C). Run each fill and ring pair through a color contrast checker before shipping a variant.
Disabled vs aria-disabled
The native disabled attribute blocks pressing and focus, keeps the button’s value out of form submission, and applies browser disabled styling automatically.
aria-disabled=”true” does less. It exposes the disabled state to assistive technology and nothing else. The button stays focusable and clickable, so a handler has to return early, and since no styling comes with it, the dimmed look is yours to write.
aria-disabled earns its place when a button must stay in the tab order, such as a submit button whose action is not available yet (MDN Web Docs). The attribute comes from ARIA, so it describes state to assistive technology and leaves behavior to your code.
WCAG 1.4.3 exempts text on inactive components from contrast requirements (W3C), which makes disabled:opacity-50 safe on native disabled buttons. MDN draws the line differently for aria-disabled buttons that stay in the focus order, since those labels still need to be legible.
Key figures to check before shipping
- 4.5:1 for label text, 3:1 for large-scale text (W3C, WCAG 2.2 SC 1.4.3)
- 3:1 for component boundaries, state changes, and author-styled focus indicators (W3C, SC 1.4.11)
- 24 by 24 CSS pixels as the minimum target size at Level AA (W3C, SC 2.5.8)
- 44 by 44 CSS pixels at Level AAA (W3C, SC 2.5.5), which is also the size MDN recommends for buttons
At the default 0.25rem spacing value and a 16px root, min-h-6 and min-w-6 give 24 pixels, and min-h-11 and min-w-11 give 44.
An icon-only button with p-2 and a size-4 icon measures 32 by 32. It clears the AA floor but falls short of the 44 pixel recommendation.
Reusing button styles: components, @apply, or @utility
In a framework project, build a button component and keep the classes inside it. In a templating language, use a template partial. Reach for custom CSS, @apply, or @utility only when a partial feels heavy or you need to override styles you do not control, which matches the order of preference in the Tailwind CSS docs.
| Method | Where styles live | Best fit | Main drawback |
|---|
| Framework component | Component file | React, Vue projects | Needs a framework |
| Template partial | Partial file | Blade, ERB, Twig, Nunjucks | Variants need template logic |
| Component class in @layer components | CSS file | One small button where a partial feels heavy | Back to writing CSS |
| @utility | CSS file | Custom class that must work with hover, focus, lg | v4 API, replaces the v3 layer approach |
| @apply | CSS file | Overriding third-party markup | Needs @reference in Vue, Svelte, CSS modules |
Component with variant props
class-variance-authority (cva) turns a variant map into a function. Besides plain variants it supports compound and default ones, and its docs cover boolean variants such as disabled.
const button = cva("inline-flex items-center rounded-md font-medium", {
variants: {
intent: {
solid: "bg-blue-600 text-white hover:bg-blue-700",
ghost: "text-blue-600 hover:bg-blue-50",
},
size: {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
},
},
defaultVariants: { intent: "solid", size: "md" },
});
- Put the classes every variant shares in the base string
- List each variant and size as its own key
- Set defaultVariants so a bare button renders the primary look
- Add compoundVariants only for combinations that need their own classes
In a React project, setting up Tailwind CSS in React comes first, and the component file then becomes the single source of truth. A Vue project follows the same pattern with a single-file component.
A shared variant map is also the first piece of a design system, since every screen reads the same intents and sizes.
@apply and @utility
@apply inlines existing utilities into your own CSS. The Tailwind docs demonstrate it on third-party library styles such as select2 dropdowns. From v4 on, Vue, Svelte and CSS modules also need @reference, because separately bundled stylesheets cannot see theme variables, custom utilities, or custom variants (Tailwind CSS upgrade guide).
@utility registers a custom class that works with hover, focus, and lg, and sorts so ordinary utilities override it (Tailwind CSS upgrade guide). A .btn utility can therefore take px-6 on a single instance without a fight.
Merging conflicting classes
Class order in the attribute decides nothing.
When two classes set the same property, the one that appears later in the generated stylesheet wins, so grid flex renders as display: grid (Tailwind CSS docs). A component that accepts a className prop cannot trust bg-red-600 to beat bg-blue-600.
tailwind-merge fixes that by keeping the last conflicting class in the string you pass it. Its README turns px-2 py-1 bg-red hover:bg-dark-red plus p-3 bg-[#B91C1C] into hover:bg-dark-red p-3 bg-[#B91C1C], and the current release, 3.7.0, supports Tailwind v4.0 through v4.3.
clsx joins conditional class strings and tailwind-merge resolves the result, so the two usually appear together.
The Tailwind docs suggest a different route. Expose specific props for style changes instead of accepting arbitrary classes. That is the better default for a button, because a closed set of intents and sizes cannot conflict.
Where Tailwind button styles break
Most breakage comes from v3 habits meeting v4 defaults, as the Tailwind CSS upgrade guide lays out.
| Default | v3 | v4 |
|---|
| Button cursor | pointer | default |
| Ring width | 3px | 1px |
| Ring color | blue-500 | currentColor |
The pointer cursor is gone
Preflight no longer gives buttons cursor: pointer, so a button shows the arrow cursor like the browser default.
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
The shadcn/ui documentation flags the same change and offers an init –pointer option for projects that want the old behavior.
Whether to restore it is a design-system call. Links styled as buttons keep the pointer either way, because browsers give every link one.
Copied focus rings look thinner
A v3 snippet with focus:ring now draws a 1px ring in the current text color instead of a 3px blue one.
- Swap ring for ring-3 to restore the width
- Add ring-blue-500 wherever the old default color was relied on
- Or set –default-ring-width and –default-ring-color in @theme, which Tailwind labels a compatibility option and not idiomatic v4
The new color default is the dangerous part. On a solid button with white label text, a currentColor ring renders white, so it vanishes against a white page and fails the 3:1 requirement for focus indicators (W3C, SC 1.4.11).
Older browsers
Tailwind v4.0 targets Safari 16.4, Chrome 111, and Firefox 128, and it will not work in older browsers.
The upgrade guide tells projects that need older browsers to stay on v3.4, because core features depend on @property and color-mix(). Check cross-browser compatibility against that list before upgrading, since the whole stylesheet depends on those features and not only the buttons.
Which prebuilt Tailwind button libraries are worth using?
Use a library when a project needs several button variants and nobody maintains a shared component. For one or two button styles, the utility strings are shorter than the install.
| Library | Pros | Cons |
|---|
| daisyUI | Short btn classes with color, style, and size modifiers; works on a, button, and input | btn-disabled only changes the look |
| Flowbite | Ready variants: default, pills, gradient, shadows, sizes; dark: variants in the markup | Long utility strings per button unless wrapped |
| shadcn/ui | Source copied into your repo; variant and size props; buttonVariants for links | React-first; you maintain the copied code |
| Headless UI | Unstyled, accessible primitive with no visual opinions | Every class is still yours to write |
| Tailwind Plus | Official components and templates from Tailwind Labs | Closed to new customers since September 9, 2026 |
Tailwind Labs announced on September 9, 2026 that it is joining Shopify and closing sign-ups for Tailwind Plus and ui.sh. Existing customers keep their access, and Tailwind CSS stays MIT-licensed.
That removes Tailwind Plus as an option for new projects.
Some fine print separates the others. daisyUI’s docs show a disabled button built with the btn-disabled class that still needs tabindex and aria-disabled added by hand. The shadcn/ui docs warn against rendering the Base UI version of Button as an anchor because it always applies role=”button”, which overrides the link role.
- For one or two button styles, write the utilities and skip the dependency
- A React app that wants to own its button source suits shadcn/ui
- Server-rendered pages that want short class names do well with daisyUI
- Flowbite gives you a ready visual set with dark variants in the markup
- If the design is yours and you only want accessible primitives, Headless UI fits
Tailwind Button FAQ
How do I make a gradient button?
Add a direction utility and color stops, for example bg-linear-to-r from-blue-500 to-indigo-600. In v4 the old bg-gradient-to-r name became bg-linear-to-r, and a variant such as dark:from-blue-500 now overrides only that stop instead of resetting the whole gradient.
Can Tailwind build a button group with joined edges?
Yes, although it has no group component. Wrap the buttons in an inline-flex container, pull the borders together with a negative margin, and use first:rounded-l-md and last:rounded-r-md so only the outer ends stay rounded.
How do I style a button for dark mode?
Add a dark: variant for each property that changes, such as dark:bg-blue-500 and dark:hover:bg-blue-400. By default the variant follows the operating system through prefers-color-scheme, so a class-based toggle needs the dark variant redefined with @custom-variant.
Does onClick work on a Tailwind-styled button in React?
Yes. onClick is an ordinary React prop, and Tailwind touches the element only through className.
Its variants are pure CSS, so hover, active, and disabled styling respond without any extra event handlers.
Is Tailwind Plus free?
No. Tailwind Plus was a one-time purchase with no subscription, and since September 9, 2026 new customers cannot buy it at all. Tailwind CSS itself stays free under the MIT license.
How to Test a Tailwind Button Before Shipping
Test in a fixed order, because each check depends on the one before it.
- Keyboard operation with Tab, Enter, and Space
- Focus ring visible on every background
- Contrast in hover, active, and disabled states
Keyboard comes first. A button that cannot receive focus makes the ring and state checks meaningless.
The trade-off is visual weight. A ring that reads on both light and dark sections usually needs an offset utility and a second color class per variant, which makes the class list longer.
This order was verified against Tailwind CSS v4.3 on October 23, 2026. A change to the focus-visible or hover defaults in a later release would shift the sequence, so rerun all three checks after each major upgrade.