Animation
Add smooth animations and transitions to HeroUI v3 components
Overview
HeroUI components are built on React Aria and support multiple animation approaches:
- Built-in animations - Components come with default CSS transitions
 - CSS animations - Customize with Tailwind utilities or custom CSS
 - JavaScript libraries - Integrate with Framer Motion, React Spring, etc.
 
Built-in Animations
HeroUI components use data attributes to expose their state for animation:
/* Popover entrance/exit */
.popover[data-entering] {
  @apply animate-in zoom-in-90 fade-in-0 duration-200;
}
.popover[data-exiting] {
  @apply animate-out zoom-out-95 fade-out duration-150;
}
/* Button press effect */
.button:active,
.button[data-pressed="true"] {
  transform: scale(0.97);
}
/* Accordion expansion */
.accordion__panel[aria-hidden="false"] {
  @apply h-[var(--panel-height)] opacity-100;
}State Attributes
Components expose these states for styling:
[data-hovered="true"]- Hover state[data-pressed="true"]- Active/pressed state[data-focus-visible="true"]- Keyboard focus[data-disabled="true"]- Disabled state[data-entering]/[data-exiting]- Transition states[aria-expanded="true"]- Expanded state
CSS Animations
Using Tailwind Utilities
// Pulse on hover
<Button className="hover:animate-pulse">
  Hover me
</Button>
// Fade in entrance
<Alert className="animate-fade-in">
  Welcome message
</Alert>
// Staggered list
<div className="space-y-2">
  <Card className="animate-fade-in animate-delay-100">Item 1</Card>
  <Card className="animate-fade-in animate-delay-200">Item 2</Card>
</div>Custom Transitions
Override default component animations:
/* Slower accordion */
.accordion__panel {
  @apply transition-all duration-500;
}
/* Bouncy button */
.button:active {
  animation: bounce 0.3s;
}
@keyframes bounce {
  50% { transform: scale(0.95); }
}Framer Motion
HeroUI components work seamlessly with Framer Motion for advanced animations.
Basic Usage
import { motion } from 'framer-motion';
import { Button } from '@heroui/react';
const MotionButton = motion(Button);
<MotionButton
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
>
  Animated Button
</MotionButton>Entrance Animations
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  <Alert>
    <Alert.Title>Welcome!</Alert.Title>
  </Alert>
</motion.div>Layout Animations
import { AnimatePresence, motion } from 'framer-motion';
function Tabs({ items, selected }) {
  return (
    <div className="flex gap-2">
      {items.map((item, i) => (
        <Button key={i} onPress={() => setSelected(i)}>
          {item}
          {selected === i && (
            <motion.div
              layoutId="active"
              className="absolute inset-0 bg-accent"
              transition={{ type: "spring", bounce: 0.2 }}
            />
          )}
        </Button>
      ))}
    </div>
  );
}Render Props
Use render props to apply dynamic animations based on component state:
<Button>
  {({ isPressed, isHovered }) => (
    <motion.span
      animate={{
        scale: isPressed ? 0.95 : isHovered ? 1.05 : 1
      }}
    >
      Interactive Button
    </motion.span>
  )}
</Button>Accessibility
Respecting Motion Preferences
Always respect user motion preferences:
@media (prefers-reduced-motion: reduce) {
  .button {
    transition: none;
  }
  .animate-* {
    animation: none !important;
  }
}With Framer Motion:
import { useReducedMotion } from 'framer-motion';
function AnimatedCard() {
  const shouldReduceMotion = useReducedMotion();
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
    >
      <Card>Content</Card>
    </motion.div>
  );
}Performance Tips
Use GPU-Accelerated Properties
Prefer transform and opacity for smooth animations:
/* Good - GPU accelerated */
.slide-in {
  transform: translateX(-100%);
  transition: transform 0.3s;
}
/* Avoid - Triggers layout */
.slide-in {
  left: -100%;
  transition: left 0.3s;
}Will-Change Optimization
.button {
  will-change: transform;
}
.button:not(:hover) {
  will-change: auto;
}