Button
Migration guide for Button from HeroUI v2 to v3
Refer to the v3 Button documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.
Structure Changes
In v2, Button used a combination of color and variant props:
import { Button } from "@heroui/react";
export default function App() {
return <Button color="primary" variant="solid">Button</Button>;
}In v3, Button uses only the variant prop (no separate color prop):
import { Button } from "@heroui/react";
export default function App() {
return <Button variant="primary">Button</Button>;
}Key Changes
1. Variants and Colors
v2: Used color + variant combination
v3: Uses variant only (no separate color prop)
| v2 Color + Variant | v3 Variant | Notes |
|---|---|---|
color="primary" variant="solid" | variant="primary" | Default primary button |
color="default" variant="solid" | variant="primary" | Use primary variant |
color="secondary" variant="solid" | variant="secondary" | Same |
color="success" variant="solid" | variant="primary" | Use primary with custom styling if needed |
color="warning" variant="solid" | variant="primary" | Use primary with custom styling if needed |
color="danger" variant="solid" | variant="danger" | Danger variant available |
color="primary" variant="bordered" | variant="secondary" | Similar appearance |
color="primary" variant="light" | variant="tertiary" | Similar appearance |
color="primary" variant="flat" | variant="tertiary" | Similar appearance |
color="primary" variant="faded" | variant="secondary" | Similar appearance |
color="primary" variant="ghost" | variant="ghost" | Same |
color="danger" variant="flat" | variant="danger-soft" | New soft danger variant |
v2 Variants: solid, bordered, light, flat, faded, shadow, ghost
v3 Variants: primary, secondary, tertiary, outline, ghost, danger, danger-soft
2. Loading State: isLoading → isPending
v2: Used isLoading prop
v3: Uses isPending prop
3. Default Width Behavior
v2: Buttons had minimum widths based on size (min-w-16 for sm, min-w-20 for md, min-w-24 for lg)
v3: Buttons use w-fit by default (width fits content, no minimum width)
This means v3 buttons will be narrower than v2 buttons when they have short text. To maintain v2's minimum width behavior, add Tailwind classes see the Minimum Width example section.
4. Prop Changes
| v2 Prop | v3 Location | Notes |
|---|---|---|
isLoading | Button | Renamed to isPending |
isIconOnly | Button | Still available in v3 — renders a square button with only an icon |
color | - | Removed (variants handle styling) |
radius | - | Removed (use Tailwind e.g. rounded-lg) |
startContent, endContent | - | Place icons as children |
spinner, spinnerPlacement | - | Handle loading manually with render props |
disableRipple | - | Removed (ripple removed in v3) |
disableAnimation | - | Removed (animations handled internally) |
classNames | - | Use className |
5. ButtonGroup Available
v2: Had a dedicated ButtonGroup component
v3: ButtonGroup continues to exist. See the ButtonGroup migration guide for details.
Migration Examples
Variants
<Button color="primary" variant="solid">Solid</Button>
<Button color="primary" variant="bordered">Bordered</Button>
<Button color="primary" variant="ghost">Ghost</Button><Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>Danger Soft Variant
The danger-soft variant is new in v3. It replaces the v2 pattern of using a flat danger button.
<Button color="danger" variant="flat">Delete</Button><Button variant="danger-soft">Delete</Button>Icon Only (isIconOnly)
The isIconOnly prop is available in both v2 and v3. It renders a square button sized to fit a single icon. In v3, use variant instead of color for styling.
<Button isIconOnly color="danger" variant="flat">
<HeartIcon />
</Button>
<Button isIconOnly color="primary" variant="bordered" size="sm">
<SearchIcon />
</Button>import { Icon } from "@iconify/react";
<Button isIconOnly variant="danger-soft">
<Icon icon="gravity-ui:heart" />
</Button>
<Button isIconOnly variant="secondary" size="sm">
<Icon icon="gravity-ui:magnifier" />
</Button>Loading State
{/* Simple loading */}
<Button isLoading color="primary">
Loading
</Button>
{/* Loading with conditional content */}
<Button
isLoading={isLoading}
spinnerPlacement="start"
color="primary"
onPress={() => setIsLoading(true)}
>
Upload File
</Button>import { useState } from "react";
import { Spinner } from "@heroui/react";
import { Icon } from "@iconify/react";
const [isLoading, setIsLoading] = useState(false);
{/* Simple loading */}
<Button isPending={isLoading}>
{({isPending}) => (
<>
{isPending && <Spinner color="current" size="sm" />}
Loading
</>
)}
</Button>
{/* Loading with conditional content */}
<Button
isPending={isLoading}
onPress={() => setIsLoading(true)}
>
{({isPending}) => (
<>
{isPending ? (
<Spinner color="current" size="sm" />
) : (
<Icon icon="gravity-ui:paperclip" />
)}
{isPending ? "Uploading..." : "Upload File"}
</>
)}
</Button>With Icons
import { Icon } from "@iconify/react";
<Button
color="success"
endContent={<Icon icon="gravity-ui:camera" />}
>
Take a photo
</Button>
<Button
color="danger"
startContent={<Icon icon="gravity-ui:trash-bin" />}
variant="bordered"
>
Delete user
</Button>import { Icon } from "@iconify/react";
<Button variant="primary">
<Icon icon="gravity-ui:camera" />
Take a photo
</Button>
<Button variant="secondary">
<Icon icon="gravity-ui:trash-bin" />
Delete user
</Button>Icon Only Button
<Button isIconOnly color="danger">
<HeartIcon />
</Button>import { Icon } from "@iconify/react";
<Button isIconOnly variant="danger">
<Icon icon="gravity-ui:heart" />
</Button>Button Group
import { Button, ButtonGroup } from "@heroui/react";
<ButtonGroup size="sm" color="primary" variant="solid">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>import { Button, ButtonGroup } from "@heroui/react";
<ButtonGroup size="sm" variant="primary">
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>Sizes and Minimum Width
{/* v2 automatically applies minimum widths */}
<Button size="md" color="primary">Save</Button>{/* v3 uses w-fit by default - add min-width to match v2 */}
<Button size="md" variant="primary" className="min-w-20">
Save
</Button>Render Props Pattern
v3 Button supports a render prop pattern that provides state information:
<Button isPending={isLoading}>
{({isPending, isPressed, isHovered, isFocused, isFocusVisible, isDisabled}) => (
<>
{isPending && <Spinner size="sm" />}
{isPressed ? "Pressed!" : "Click me"}
</>
)}
</Button>Available render props:
isPending- Whether button is in loading stateisPressed- Whether button is currently pressedisHovered- Whether button is hoveredisFocused- Whether button is focusedisFocusVisible- Whether button should show focus indicatorisDisabled- Whether button is disabled
Summary
- Color Prop Removed: Use
variantprop instead ofcolor+variant - Variants Changed: New variant system (
primary,secondary,tertiary,outline,ghost,danger,danger-soft) danger-softVariant: New in v3, replaces v2'scolor="danger" variant="flat"pattern- isLoading → isPending: Loading prop renamed
- isIconOnly: Still supported in v3 — renders a square button for icon-only use
- Default Width Changed: Buttons now use
w-fitinstead of minimum widths - addmin-w-*classes to match v2 behavior - Icons:
startContent/endContentremoved - place icons as children - Loading Spinner: Must handle spinner manually with render props
- Render Props: v3 Button children can be a function receiving
isPending,isPressed,isHovered,isFocused,isFocusVisible, andisDisabled - Radius Removed: Use Tailwind CSS classes
- Ripple Removed: No ripple effect in v3
- ButtonGroup Available: See ButtonGroup migration guide
- ClassNames Removed: Use
classNameprop