Button React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps.
Press it and it dips on a quick spring, then settles back; with a mouse the surface tints on hover. Tab to it for a focus ring that never touches the edge. Tap the playground button to see it load: the spinner takes the label's place and the width holds still. On a phone the medium size grows to 44px so it is easy to hit.
Button is a free React component in the React button components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add button, together with Motion presets, Tones and UIProvider and the npm packages @radix-ui/react-slot and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add buttonUsage
Import Button from @/components/ui/button.
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
export function CheckoutActions({ paying, onPay }: { paying: boolean; onPay: () => void }) {
return (
<div className="flex gap-2">
<Button variant="ghost">Cancel</Button>
<Button tone="brand" loading={paying} loadingText="Paying" rightIcon={<ArrowRight />} onClick={onPay}>
Pay now
</Button>
</div>
)
}Props
childrendefault"Continue"ReactNodeThe label. For icon sizes pass the icon itself and an aria-label.
variantdefault"solid""solid" | "soft" | "outline" | "ghost" | "link"solid for the one main action on a screen, soft or outline for secondary actions, ghost for toolbars and dense rows, link for actions inside text (always underlined, so it reads as a link without color or hover).
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Semantic color from the design tokens. neutral is black in light mode and white in dark mode; danger is for destructive actions.
sizedefault"md""sm" | "md" | "lg" | "icon-sm" | "icon" | "icon-lg"Height 32 / 40 / 48px with matching padding, type and icon size. The icon sizes are square. On touch screens md grows to 44px and sm keeps its look with a 44px hit area.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape. Scales with the size (md is 8 / 10 / 12px) so every size reads as the same shape; full makes a pill.
colordefault""stringAny CSS color. Replaces the tone in every variant; text on a solid fill turns black or white by lightness. Leave empty to use the tone.
loadingdefaultfalsebooleanShows a spinner in place of the label, keeps the width, sets aria-busy and ignores clicks. The button stays focusable, so keyboard focus is not lost.
loadingTextdefault""ReactNodeText next to the spinner while loading, like "Saving". The button reserves room for it, so it never changes width.
disableddefaultfalsebooleanDims the button and blocks it. With asChild the link gets aria-disabled and leaves the tab order.
fullWidthdefaultfalsebooleanStretches to the container width. The usual choice for the main action at the bottom of a phone screen.
asChilddefaultfalsebooleanRenders your single child (an <a>, a router Link) with the button's look, press and icons, instead of a <button>. The demo swaps in a link.
leftIconReactNodeAn icon before the label. Unsized svgs follow the button size (14 / 16 / 18px).
rightIconReactNodeAn icon after the label, like an arrow or a chevron.
spinnerReactNodeReplaces the default spinner.
typedefault"button""button" | "submit" | "reset"Defaults to "button" so a button inside a form never submits by accident. Set "submit" for the form's main action.
onClick(event: MouseEvent<HTMLButtonElement>) => voidNot called while loading or disabled.
classNamesPartial<Record<"content" | "icon" | "spinner", string>>Extra classes for the inner parts: the label wrapper, each icon wrapper, the spinner.
classNamestringExtra classes for the button itself, merged last. Override the tone vars here, e.g. [--tone-foreground:white].
Examples
Variants and tones
Five variants across the six tones, all driven by the design tokens.
Sizes
32, 40 and 48px, plus square icon sizes. Unsized icons follow along.
Loading
The spinner takes the label's place and the width holds. loadingText gets room reserved up front.
Icon buttons
Always with an aria-label. aria-pressed turns one into a toggle.
As a link
asChild renders your <a> or router Link with the button's look and press.
Custom color
Any CSS color. Text on the fill turns black or white on its own; soft and outline mix it for contrast.
Radius
none to full; each step scales with the size.
Checkout on a phone
fullWidth, lg, and a price formatted in dollars.
Dependencies
Files
- components/ui/button.tsx
FAQ
Is the React Button component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Button?
Run npx wingo-ui@latest add button. It also adds the Wingo UI items it builds on (Motion presets, Tones and UIProvider) and the npm packages @radix-ui/react-slot and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/button.json.
How does Button behave on phones?
On a phone the medium size grows to 44px so it is easy to hit.
See also
Used in
More React button components
Articles using this component
- Antigravity MCP Setup: The Right Config Path and Prompts
Building UI with AI agents10 min read
- Codex Frontend Design Skill: Pair It With Real Components
Building UI with AI agents10 min read
- Is shadcn Free for Commercial Use? MIT vs Paid Kits
Comparisons and alternatives10 min read
- OpenCode MCP Setup: opencode.json for v1 and v2
Building UI with AI agents11 min read