Animation
Add smooth animations and transitions to HeroUI v3 components
HeroUI components support multiple animation approaches: built-in CSS transitions, custom CSS animations, and JavaScript libraries like Framer Motion.
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 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:
/* 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 '@lenso/ui';
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
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: HeroUI automatically respects user motion preferences using Tailwind's motion-reduce: utility. All built-in transitions and animations are disabled when users enable "reduce motion" in their system settings.
HeroUI extends Tailwind's motion-reduce: variant to support both the native prefers-reduced-motion media query and the data-reduce-motion attribute.
/* HeroUI pattern - uses Tailwind's motion-reduce: */.button { @apply transition-colors motion-reduce:transition-none;}
/* Expands to support both approaches: */@media (prefers-reduced-motion: reduce) { .button { transition: none; }}
[data-reduce-motion="true"] .button { transition: none;}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> );}Disabling animations globally: Add data-reduce-motion="true" to the <html> or <body> tag:
<html data-reduce-motion="true"> <!-- All HeroUI animations will be disabled --></html>HeroUI automatically detects the user's prefers-reduced-motion: reduce setting and disables animations accordingly.
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: Use will-change to optimize animations, but remove it when not animating:
.button { will-change: transform;}
.button:not(:hover) { will-change: auto;}