No description
|
Some checks failed
Publish / publish (push) Failing after 0s
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| .gitignore | ||
| eslint.config.js | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.tsbuildinfo | ||
@lilith/ui-motion
Animation utilities and hooks for framer-motion. Provides reusable patterns for count-up, parallax, staggered, and floating animations.
Features
- useCountUp - animated number counting with scroll trigger
- useMultiLayerParallax - multi-layer parallax scrolling effects
- useStaggeredAnimation - staggered entrance animations for lists
- useFloatingAnimation - continuous floating/bobbing animations
- parseStatValue - utility for parsing stat values from strings
Installation
pnpm add @lilith/ui-motion
Peer Dependencies
{
"react": "^18.0.0",
"react-dom": "^18.0.0",
"framer-motion": "^11.0.0"
}
Usage
useCountUp
Animate numbers counting up when element enters viewport:
import { useCountUp } from '@lilith/ui-motion';
function StatCard({ value }: { value: number }) {
const { value: animatedValue, ref } = useCountUp(value, 2000);
return (
<div>
<span ref={ref}>{animatedValue}</span>
</div>
);
}
// Usage
<StatCard value={1500} />
// Displays: 0 → 1 → 15 → 150 → 500 → 1000 → 1500 (animated)
With options:
// Custom duration (3 seconds)
const { value, ref } = useCountUp(1000, 3000);
// Start immediately (don't wait for viewport)
const { value, ref } = useCountUp(1000, 2000, false);
useMultiLayerParallax
Create parallax scrolling effects with multiple layers:
import { useMultiLayerParallax } from '@lilith/ui-motion';
import { motion } from 'framer-motion';
function ParallaxScene() {
const { containerRef, layers } = useMultiLayerParallax({
layers: [
{ speed: 0.1 }, // Background - slow
{ speed: 0.3 }, // Midground
{ speed: 0.6 }, // Foreground - fast
],
});
return (
<div ref={containerRef} style={{ height: '100vh', overflow: 'hidden' }}>
<motion.div style={{ y: layers[0].y }}>
<img src="/mountains.jpg" alt="Background" />
</motion.div>
<motion.div style={{ y: layers[1].y }}>
<img src="/trees.jpg" alt="Midground" />
</motion.div>
<motion.div style={{ y: layers[2].y }}>
<img src="/character.jpg" alt="Foreground" />
</motion.div>
</div>
);
}
useStaggeredAnimation
Stagger animations for list items:
import { useStaggeredAnimation } from '@lilith/ui-motion';
import { motion } from 'framer-motion';
function AnimatedList({ items }: { items: string[] }) {
const { containerVariants, itemVariants } = useStaggeredAnimation({
staggerDelay: 0.1,
duration: 0.4,
});
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item} variants={itemVariants}>
{item}
</motion.li>
))}
</motion.ul>
);
}
Custom animation variants:
const { containerVariants, itemVariants } = useStaggeredAnimation({
staggerDelay: 0.15,
duration: 0.5,
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
});
useFloatingAnimation
Create continuous floating/bobbing animations:
import { useFloatingAnimation } from '@lilith/ui-motion';
import { motion } from 'framer-motion';
function FloatingElement({ index }: { index: number }) {
const floatingProps = useFloatingAnimation(index, 20, 6);
return (
<motion.div animate={floatingProps}>
Floating element {index}
</motion.div>
);
}
// Multiple elements with staggered floating
function FloatingCloud() {
return (
<div>
<FloatingElement index={0} />
<FloatingElement index={1} />
<FloatingElement index={2} />
</div>
);
}
With custom parameters:
// Smaller amplitude, faster animation
const floatingProps = useFloatingAnimation(0, 10, 3);
// Larger amplitude, slower animation
const floatingProps = useFloatingAnimation(0, 40, 10);
parseStatValue
Parse stat values from formatted strings:
import { parseStatValue } from '@lilith/ui-motion';
parseStatValue('1.5K'); // 1500
parseStatValue('2.3M'); // 2300000
parseStatValue('$99.99'); // 99.99
parseStatValue('42'); // 42
parseStatValue('N/A'); // 0
API Reference
useCountUp
function useCountUp(
end: number,
duration?: number, // Default: 2000ms
startOnView?: boolean // Default: true
): {
value: number;
ref: React.RefObject<HTMLSpanElement>;
}
| Parameter | Type | Default | Description |
|---|---|---|---|
end |
number |
required | Target number |
duration |
number |
2000 |
Animation duration (ms) |
startOnView |
boolean |
true |
Wait for element to be visible |
useMultiLayerParallax
function useMultiLayerParallax(config: {
layers: Array<{ speed: number }>;
}): MultiLayerParallaxReturn;
interface MultiLayerParallaxReturn {
containerRef: React.RefObject<HTMLDivElement>;
layers: Array<{
y: MotionValue<number>;
speed: number;
}>;
}
useStaggeredAnimation
function useStaggeredAnimation(config?: {
staggerDelay?: number; // Default: 0.1
duration?: number; // Default: 0.4
hidden?: TargetAndTransition;
visible?: TargetAndTransition;
}): StaggeredAnimationVariants;
interface StaggeredAnimationVariants {
containerVariants: Variants;
itemVariants: Variants;
}
useFloatingAnimation
function useFloatingAnimation(
index?: number, // Default: 0
amplitude?: number, // Default: 20
duration?: number // Default: 6
): FloatingAnimationProps;
type FloatingAnimationProps = {
y: number[];
x: number[];
rotate: number[];
transition: {
duration: number;
repeat: number;
ease: string;
delay: number;
};
};
Types
import type {
MultiLayerParallaxReturn,
StaggeredAnimationVariants,
FloatingAnimationProps,
} from '@lilith/ui-motion';
Integration with framer-motion
All hooks are designed to work seamlessly with framer-motion's motion components:
import { motion } from 'framer-motion';
import { useCountUp, useFloatingAnimation, useStaggeredAnimation } from '@lilith/ui-motion';
// Count-up with motion
function AnimatedStat({ value }: { value: number }) {
const { value: count, ref } = useCountUp(value);
return (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
>
<span ref={ref}>{count}</span>
</motion.div>
);
}
// Floating with motion
function Floater({ index }: { index: number }) {
const floating = useFloatingAnimation(index);
return <motion.div animate={floating}>Float</motion.div>;
}
// Staggered list with motion
function List({ items }: { items: string[] }) {
const { containerVariants, itemVariants } = useStaggeredAnimation();
return (
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item} variants={itemVariants}>{item}</motion.li>
))}
</motion.ul>
);
}
License
MIT