Icon Button

A button that bursts into particles when clicked.

"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { motion, useReducedMotion } from "motion/react"
import {
  IconButton,
  IconButtonParticles,
} from "@animate-ui/react/primitives/buttons/icon"
import { FavouriteIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

const iconButtonVariants = cva(
  "focus-visible:border-ring focus-visible:ring-ring/50 inline-flex shrink-0 items-center justify-center rounded-lg transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        accent: "bg-accent text-accent-foreground hover:bg-accent/90",
        outline:
          "bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
        ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50",
      },
      size: {
        default: "size-9",
        sm: "size-8",
        lg: "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export default function IconButtonDemo({
  variant = "outline",
  size = "default",
}: VariantProps<typeof iconButtonVariants>) {
  const [liked, setLiked] = React.useState(false)
  const reduce = useReducedMotion()

  return (
    <IconButton
      aria-label="Like"
      aria-pressed={liked}
      onClick={() => setLiked((value) => !value)}
      className={cn(iconButtonVariants({ variant, size }))}
    >
      <motion.span
        className="inline-flex"
        initial={false}
        animate={{ scale: reduce ? 1 : liked ? [1, 1.3, 1] : [1, 0.85, 1] }}
        transition={{
          duration: 0.4,
          times: [0, 0.4, 1],
          ease: ["easeOut", [0.34, 1.56, 0.64, 1]],
        }}
      >
        <HugeiconsIcon
          icon={FavouriteIcon}
          strokeWidth={2}
          className={cn(
            "fill-transparent transition-[fill,color] duration-200 ease-out",
            liked && "fill-red-500 text-red-500"
          )}
        />
      </motion.span>
      {liked && <IconButtonParticles className="text-red-500" />}
    </IconButton>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Usage

import {
  IconButton,
  IconButtonParticles,
} from '@animate-ui/react/primitives/buttons/icon';

export function Star() {
  return (
    <IconButton aria-label="Star">
      <StarIcon />
      <IconButtonParticles className="text-neutral-500" />
    </IconButton>
  );
}

Animation

  • Each click bursts particles from the top edge of the button (built on Particles): 6 dots fly 30px out around it (spread evenly at the full 360°), growing then shrinking back to nothing as they fade in and out, over 0.8s with an easeOut, one after another 50ms apart. A click during a burst starts it over.
  • Pressing scales the button to tapScale (0.97), on Motion's spring for scale (stiffness: 550, damping: 30). It doesn't scale on hover, unless you set hoverScale.
  • <AnimateUIProvider tempo> paces the press spring.
  • Under reduced motion, the particles don't burst and the button doesn't scale.

API Reference

IconButton

PropTypeDefault
hoverScale?
number1
tapScale?
number0.97
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }
asChild?
booleanfalse
...props?
HTMLMotionProps<'button'>-

useIconButton

Returns bursts, the number of clicks so far: each one starts a new burst.

IconButtonParticles

PropTypeDefault
size?
number4
side?
'top' | 'bottom' | 'left' | 'right''top'
sideOffset?
number0
align?
'start' | 'center' | 'end''center'
alignOffset?
number0
count?
number6
radius?
number30
spread?
number360
duration?
number0.8
holdDelay?
number0.05
delay?
number0
...props?
ParticlesEffectProps-

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page