Button

Trigger actions with clear hierarchy, consistent states, and full token control across every use case.

Code

Button takes the same props as Ant Design v6's Button, with four additions: a squircle value on shape, palettes from your theme on color (like emerald or the gray slate), an avatar prop that puts a face in the icon slot, and a loading spinner from the active icon pack. Every other prop works as it does in Ant Design.

Install

The component is copied into your project as a file you own. Run init once per project first: it installs the theme tokens and <AntuikitProvider>, and sets up the CSS layer order your class overrides depend on.
npx antuikit@latest init
npx antuikit@latest add button

Import

A registry:ui file lands under your ui alias, so the default path is @/components/ui/button.
import { Button } from "@/components/ui/button"
<Button color="primary" variant="solid">Button</Button>
Two exports come with it. ButtonProps is the props type, with shape and color widened, and buttonVariants is the cva the component composes with, for call sites that want the shape classes without the component.
import { Button, buttonVariants, type ButtonProps } from "@/components/ui/button"

color + variant

These two are the API. color says which hue, variant says how loudly, and together they reach every combination.
1
<Button color="default" variant="solid">Default</Button>
2
<Button color="primary" variant="solid">Primary</Button>
3
<Button color="primary" variant="outlined">Outlined</Button>
4
<Button color="danger" variant="filled">Delete</Button>
The six variant values, from loudest to quietest:
  • solid: filled background, highest emphasis
  • outlined: border only, medium emphasis
  • dashed: dashed border, optional or add action
  • filled: subtle tinted background, medium emphasis
  • text: label only, minimal emphasis
  • link: link styling, navigation action

The type shorthand

type is the older spelling of the same axis: it reaches five fixed pairs, where color + variant reaches all of them. Both still work.
type="primary" -> color="primary" variant="solid"
type="dashed" -> variant="dashed"
type="text" -> variant="text"
type="link" -> variant="link"
type="default" -> variant="outlined"
danger is the older boolean form of color="danger". It is dropped as soon as you set color and variant, and on its own it only gives an outlined button, so write the pair.
{/* Legacy: one button, and the variant is not yours to pick */}
<Button danger>Delete</Button>
{/* Prefer: the pair, which composes */}
<Button color="danger" variant="filled">Remove label</Button>
<Button color="danger" variant="outlined">Reset workspace</Button>
<Button color="danger" variant="solid">Delete account</Button>

Colors

color takes default, primary and danger, antd's presets (blue, green, gold and the rest), or a palette from your theme (the default theme adds amber, emerald, teal, sky, indigo, violet, fuchsia, rose and grays like slate and zinc). Each works with every variant.
Semantic
<Button color="default" variant="solid">Default</Button>
<Button color="primary" variant="solid">Primary</Button>
<Button color="danger" variant="solid">Danger</Button>
antd presets
The twelve ramps antd ships and draws itself: blue, purple, cyan, green, magenta, red, orange, yellow, volcano, geekblue, lime, gold.
<Button color="geekblue" variant="solid">Geekblue</Button>
<Button color="volcano" variant="outlined">Volcano</Button>
<Button color="lime" variant="filled">Lime</Button>
Your theme's own palettes
A palette works by name, like a preset. Besides the eight hues above, the default theme adds nine grays: slate, gray, zinc, neutral, stone, mauve, olive, mist and taupe.
<Button color="emerald" variant="solid">Emerald</Button>
<Button color="fuchsia" variant="outlined">Fuchsia</Button>
<Button color="sky" variant="filled">Sky</Button>
Add your own in the token file the theme is generated from, under palettes, and it works by name with nothing else to wire.
{
"palettes": {
"brand": "#006bff"
}
}

Sizes

Three steps, all from the theme's controlHeight.
  • large: 40px
  • medium: 32px (default)
  • small: 24px
<Button size="large" color="primary" variant="solid">Large</Button>
<Button size="medium" color="primary" variant="solid">Medium</Button>
<Button size="small" color="primary" variant="solid">Small</Button>
middle is v5's name for medium and still resolves, so old call sites keep working. New ones should write medium. Size can also come from <ConfigProvider componentSize>, and the avatar face and the squircle corner follow it too.

Shape

One prop, four shapes, one at a time.
  • default: antd's borderRadius box
  • squircle: a superellipse corner, added by the kit
  • round: a full pill
  • circle: a 50% radius and a square box, for icon-only
import { Icon } from "@/components/ui/icon"
<Button color="primary" variant="outlined">default</Button>
<Button color="primary" variant="outlined" shape="squircle">squircle</Button>
<Button color="primary" variant="outlined" shape="round">round</Button>
<Button
color="primary"
variant="outlined"
shape="circle"
aria-label="Search"
icon={<Icon name="search" />}
/>
squircle needs a recent Chromium. Other browsers draw the default corner, so never let it be the only thing telling two buttons apart. Its radius is larger than the default one, because a squircle corner looks smaller at the same radius.
There is a fifth value, square, which draws the same as default.
For a rounded-square icon button, use shape="circle" with the corner-squircle class:
<Button shape="circle" className="corner-squircle" icon={<Icon name="plus" />} aria-label="Add" />

Icons and placement

Icons come from the kit's Icon by semantic role rather than from an icon library by name, so swapping packs changes every glyph at once with nothing edited. Icon is 1em and aria-hidden by default, so it scales with the label and is not announced twice.
1
import { Button } from "@/components/ui/button"
2
import { Icon } from "@/components/ui/icon"
3
4
<Button color="primary" variant="solid" icon={<Icon name="plus" />}>
5
New project
6
</Button>
7
8
<Button icon={<Icon name="download" />}>Export</Button>
9
10
{/* Trailing glyph */}
11
<Button
12
color="primary"
13
variant="solid"
14
icon={<Icon name="arrow-right" />}
15
iconPlacement="end"
16
>
17
Continue
18
</Button>
iconPlacement is start | end and logical, so it mirrors under dir="rtl" on its own. Use it rather than iconPosition, which is deprecated and warns at runtime. <AntuikitProvider> sets the gap between glyph and label through button.classNames.icon, so both placements look even.
Icon-only
An icon with no children gives an icon-only button: a square box of the control height, or a circle with shape="circle". These need an aria-label, because there is no text for the accessible name to come from.
<Button
color="default"
variant="outlined"
icon={<Icon name="more" />}
aria-label="More actions"
/>

Avatar

avatar takes an image URL and renders it as the button's face. It sizes the image from the button and keeps the same gap to the edge, the top and the bottom at every size and shape, where an image in icon sits too far from the edge.
1
<Button shape="round" avatar="/avatars/ada.png">@ada</Button>
2
3
{/* The face follows size and shape: pass no size of its own */}
4
<Button size="small" avatar="/avatars/ada.png">Small</Button>
5
<Button size="large" shape="squircle" avatar="/avatars/ada.png">Large</Button>
6
7
{/* Trailing, and the padding cut moves with it */}
8
<Button avatar="/avatars/ada.png" iconPlacement="end">Account</Button>
9
10
{/* No label: an icon-only button, so give it an aria-label */}
11
<Button shape="circle" avatar="/avatars/ada.png" aria-label="Open profile" />

States

Loading
loading is a boolean or a { delay, icon } config. The spinner replaces whatever is in the icon slot, so a button with a glyph loses it while busy.
{/* The kit's own spinner, from the active icon pack */}
<Button color="primary" variant="solid" loading>Saving</Button>
{/* Only show the spinner if the work outlasts 300ms */}
<Button color="primary" variant="solid" loading={{ delay: 300 }}>Saving</Button>
{/* Your own spinner */}
<Button
color="primary"
variant="solid"
loading={{ icon: <Icon name="loading" className="animate-spin" /> }}
>
Processing
</Button>
Disabled
<Button color="primary" variant="solid" disabled>Disabled</Button>
<Button color="default" variant="outlined" disabled>Disabled</Button>
{/* A disabled href button renders an <a> with no href, so it cannot be
focused or followed */}
<Button href="/pricing" disabled>Disabled link</Button>
The disabled fill
defaultBgDisabled and dashedBgDisabled set the disabled background of default and dashed buttons. Pass var(--ant-*) values so one setting works in light and dark mode. Solid buttons read another token.
1
import { ThemeScope } from "@/components/theme-scope"
2
3
const disabledFill = {
4
components: {
5
Button: {
6
defaultBgDisabled: "var(--ant-color-fill-secondary)",
7
dashedBgDisabled: "var(--ant-color-fill)",
8
},
9
},
10
}
11
12
<ThemeScope theme={{ light: disabledFill, dark: disabledFill }}>
13
<Button disabled>Disabled</Button>
14
<Button variant="dashed" color="default" disabled>Disabled</Button>
15
</ThemeScope>

Ghost

ghost drops the background so the button reads against a colored or photographic surface.
<div className="bg-[url(/hero.jpg)] bg-cover p-6">
<Button ghost>Default</Button>
<Button color="default" variant="dashed" ghost>Dashed</Button>
<Button color="danger" variant="outlined" ghost>Danger</Button>
</div>

Block

block makes the button fill its parent's width: the mobile-sheet and form-footer case.
<Button color="primary" variant="solid" block>Continue</Button>
<Button color="default" variant="outlined" block>Cancel</Button>
Give a Button an href and it renders an <a> instead of a <button>, with the same look. variant="link" sets the look and href sets the element, and each works without the other.
{/* An anchor that looks like a button */}
<Button color="primary" variant="solid" href="https://antuikit.com" target="_blank" rel="noreferrer">
Visit antuikit
</Button>
{/* A button that looks like a link, but is still a <button> */}
<Button color="primary" variant="link" onClick={reset}>Reset filters</Button>

Grouping buttons

Space.Compact joins adjacent controls by context rather than by wrapping them, so each half keeps its own props. Button.Group and Dropdown.Button are deprecated, and the kit does not include Button.Group.
1
import { Space } from "antd"
2
import { Icon } from "@/components/ui/icon"
3
4
{/* Icon toolbar: one shared outline */}
5
<Space.Compact>
6
<Button icon={<Icon name="zoom-out" />} aria-label="Zoom out" />
7
<Button icon={<Icon name="zoom-in" />} aria-label="Zoom in" />
8
<Button icon={<Icon name="rotate-left" />} aria-label="Rotate left" />
9
<Button icon={<Icon name="rotate-right" />} aria-label="Rotate right" />
10
</Space.Compact>
A group that ends in a menu
One primary button plus n secondary ones, and past three operations the extras fold into a Dropdown. That keeps the primary action one click away while the rest stay one click deeper.
1
import { Dropdown, Space } from "antd"
2
import { Icon } from "@/components/ui/icon"
3
4
const items = [
5
{ key: "draft", label: "Save as draft" },
6
{ key: "template", label: "Save as template" },
7
]
8
9
<Space.Compact>
10
<Button color="primary" variant="solid" onClick={publish}>Publish</Button>
11
<Dropdown menu={{ items }} trigger={["click"]}>
12
<Button
13
color="primary"
14
variant="solid"
15
icon={<Icon name="chevron-down" />}
16
aria-label="More publish options"
17
/>
18
</Dropdown>
19
</Space.Compact>

With a count

Badge wraps the control rather than the control growing a count prop, which is why the same treatment works on icon-only and labeled buttons alike.
import { Badge } from "antd"
<Badge count={5}>
<Button icon={<Icon name="filter" />} aria-label="Filters" />
</Badge>
<Badge count={5}>
<Button icon={<Icon name="filter" />}>Filters</Button>
</Badge>

Styling the parts

classNames and styles are objects keyed by part, and both also accept a function returning that object. Your classNames.icon is merged with the icon class AntuikitProvider sets, so the gap to the label stays even.
1
<Button
2
color="primary"
3
variant="solid"
4
icon={<Icon name="star" />}
5
classNames={{
6
root: "shadow-fancy",
7
icon: "text-warning",
8
content: "tracking-wide",
9
}}
10
styles={{ content: { fontVariantNumeric: "tabular-nums" } }}
11
>
12
Generate
13
</Button>
Semantic DOM
The three keys classNames and styles accept. An unknown key is ignored.
A plain className is usually enough. Button adds no colors of its own, so a class in className overrides antd's styles without !important.
{/* Overrides the background, with no ! */}
<Button className="bg-linear-to-r from-primary to-purple-6 border-transparent text-white">
Gradient
</Button>

Provider-level settings

Two things that look like Button props but are not. The click ripple is a ConfigProvider concern, and so is a default color/variant pair for a whole region.
1
import { ConfigProvider } from "antd"
2
3
<ConfigProvider
4
wave={{ showEffect: (node, { component }) => { /* your animation */ } }}
5
button={{ color: "primary", variant: "filled" }}
6
>
7
<Button>Inherits the pair</Button>
8
</ConfigProvider>
For a differently themed region, use ThemeScope. A nested ConfigProvider re-themes antd's components but not the plain elements beside them, and ThemeScope themes both. It takes one theme per mode.
import { ThemeScope } from "@/components/theme-scope"
const violet = { token: { colorPrimary: "#7c3aed" } }
<ThemeScope theme={{ light: violet, dark: violet }}>
<Button color="primary" variant="solid">Scoped</Button>
<span className="text-primary">and this reads the scope too</span>
</ThemeScope>

API Reference

Button
Button's props. Rows marked antuikit in the Version column are added by the kit.