Icon Button

A button that bursts into particles when clicked.

<script setup lang="ts">
import { FavouriteIcon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import { IconButton, IconButtonParticles } from '@animate-ui/vue/primitives/buttons/icon'
import { useShouldReduceMotion } from '@animate-ui/vue/utils/motion'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from 'cn'
import { motion } from 'motion-v'
import { ref } from 'vue'

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',
    },
  },
)

type IconButtonVariants = VariantProps<typeof iconButtonVariants>

withDefaults(
  defineProps<{
    variant?: IconButtonVariants['variant']
    size?: IconButtonVariants['size']
  }>(),
  { variant: 'outline', size: 'default' },
)

const liked = ref(false)
const reduce = useShouldReduceMotion()
</script>

<template>
  <IconButton
    aria-label="Like"
    :aria-pressed="liked"
    :class="cn(iconButtonVariants({ variant, size }))"
    @click="liked = !liked"
  >
    <motion.span
      class="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"
        :stroke-width="2"
        :class="cn('fill-transparent transition-[fill,color] duration-200 ease-out', liked && 'fill-red-500 text-red-500')"
      />
    </motion.span>
    <IconButtonParticles v-if="liked" class="text-red-500" />
  </IconButton>
</template>

Installation

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

Usage

<script setup lang="ts">
import { IconButton, IconButtonParticles } from '@animate-ui/vue/primitives/buttons/icon';
</script>

<template>
  <IconButton aria-label="Star">
    <StarIcon />
    <IconButtonParticles class="text-neutral-500" />
  </IconButton>
</template>

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 tap-scale (0.97), on Motion's spring for scale (stiffness: 550, damping: 30). It doesn't scale on hover, unless you set hover-scale.
  • <AnimateUIProvider :tempo> paces the press spring.
  • Under reduced motion, the particles don't burst and the button doesn't scale.

API Reference

IconButton

PropTypeDefault
hover-scale?
number1
tap-scale?
number0.97
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }
as-child?
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'
side-offset?
number0
align?
'start' | 'center' | 'end''center'
align-offset?
number0
count?
number6
radius?
number30
spread?
number360
duration?
number0.8
hold-delay?
number0.05
delay?
number0
...props?
ParticlesEffectProps-

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

Last updated: 10/23/2026