Pro--% off in--d : --h : --m : --s
HeroUI

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 + Variantv3 VariantNotes
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: isLoadingisPending

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 Propv3 LocationNotes
isLoadingButtonRenamed to isPending
isIconOnlyButtonStill 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 state
  • isPressed - Whether button is currently pressed
  • isHovered - Whether button is hovered
  • isFocused - Whether button is focused
  • isFocusVisible - Whether button should show focus indicator
  • isDisabled - Whether button is disabled

Summary

  1. Color Prop Removed: Use variant prop instead of color + variant
  2. Variants Changed: New variant system (primary, secondary, tertiary, outline, ghost, danger, danger-soft)
  3. danger-soft Variant: New in v3, replaces v2's color="danger" variant="flat" pattern
  4. isLoading → isPending: Loading prop renamed
  5. isIconOnly: Still supported in v3 — renders a square button for icon-only use
  6. Default Width Changed: Buttons now use w-fit instead of minimum widths - add min-w-* classes to match v2 behavior
  7. Icons: startContent/endContent removed - place icons as children
  8. Loading Spinner: Must handle spinner manually with render props
  9. Render Props: v3 Button children can be a function receiving isPending, isPressed, isHovered, isFocused, isFocusVisible, and isDisabled
  10. Radius Removed: Use Tailwind CSS classes
  11. Ripple Removed: No ripple effect in v3
  12. ButtonGroup Available: See ButtonGroup migration guide
  13. ClassNames Removed: Use className prop

On this page