A lightweight, performant React library for adding smooth spring animations to any element. Built with TypeScript and optimized for modern React applications.
- π― Simple API - Easy to use hooks and components
- β‘ High Performance - Uses
requestAnimationFrame
for smooth 60fps animations - π¨ Flexible - Animate any CSS property with spring physics
- π¦ Lightweight - No external dependencies, just React
- π§ TypeScript - Full TypeScript support with type safety
- πͺ Preset Configurations - Pre-configured spring settings for common use cases
- π Zero Configuration - Works out of the box with any React project
npm install primotion
# or
yarn add primotion
# or
pnpm add primotion
import React, { useState } from 'react';
import { useSpring } from 'primotion';
function AnimatedCounter() {
const [count, setCount] = useState(0);
const { value, animate } = useSpring({ from: 0, to: count });
const increment = () => {
setCount(prev => prev + 1);
animate(count + 1);
};
return (
<div>
<h1>{Math.round(value)}</h1>
<button onClick={increment}>Increment</button>
</div>
);
}
import React from 'react';
import { FadeIn, SlideIn } from 'primotion';
function App() {
return (
<div>
<FadeIn delay={200}>
<h1>This will fade in!</h1>
</FadeIn>
<SlideIn direction="up" delay={400}>
<p>This will slide up from below!</p>
</SlideIn>
</div>
);
}
import React from 'react';
import { Spring } from 'primotion';
function AnimatedBox() {
return (
<Spring from={0} to={100} immediate>
<div style={{ padding: '20px', background: 'blue', color: 'white' }}>
This box will slide down from the top!
</div>
</Spring>
);
}
A React hook that provides spring animation functionality.
Property | Type | Default | Description |
---|---|---|---|
from |
number |
0 |
Starting value |
to |
number |
0 |
Target value |
config |
SpringConfig |
{} |
Spring configuration |
immediate |
boolean |
false |
Start animation immediately |
delay |
number |
0 |
Delay before starting animation (ms) |
onUpdate |
(value: number) => void |
undefined |
Callback on each animation frame |
onComplete |
() => void |
undefined |
Callback when animation completes |
Property | Type | Description |
---|---|---|
value |
number |
Current animated value |
setValue |
(value: number) => void |
Set value immediately |
animate |
(to: number, config?: SpringConfig) => void |
Animate to a new value |
stop |
() => void |
Stop current animation |
isAnimating |
boolean |
Whether animation is currently running |
A component that wraps elements with spring animation capabilities.
Property | Type | Default | Description |
---|---|---|---|
from |
number |
0 |
Starting value |
to |
number |
0 |
Target value |
config |
SpringConfig |
{} |
Spring configuration |
immediate |
boolean |
false |
Start animation immediately |
delay |
number |
0 |
Delay before starting animation |
onUpdate |
(value: number) => void |
undefined |
Update callback |
onComplete |
() => void |
undefined |
Complete callback |
style |
React.CSSProperties |
{} |
Additional styles |
className |
string |
'' |
CSS class name |
as |
keyof JSX.IntrinsicElements |
'div' |
HTML element to render |
A component for fade-in animations.
Property | Type | Default | Description |
---|---|---|---|
children |
ReactNode |
- | Content to animate |
duration |
number |
500 |
Animation duration (ms) |
delay |
number |
0 |
Delay before starting |
config |
SpringConfig |
{} |
Spring configuration |
style |
React.CSSProperties |
{} |
Additional styles |
className |
string |
'' |
CSS class name |
as |
keyof JSX.IntrinsicElements |
'div' |
HTML element to render |
A component for slide-in animations.
Property | Type | Default | Description |
---|---|---|---|
children |
ReactNode |
- | Content to animate |
direction |
'up' | 'down' | 'left' | 'right' |
'up' |
Slide direction |
distance |
number |
50 |
Distance to slide (px) |
delay |
number |
0 |
Delay before starting |
config |
SpringConfig |
{} |
Spring configuration |
style |
React.CSSProperties |
{} |
Additional styles |
className |
string |
'' |
CSS class name |
as |
keyof JSX.IntrinsicElements |
'div' |
HTML element to render |
interface SpringConfig {
tension?: number; // Spring stiffness (default: 170)
friction?: number; // Damping factor (default: 26)
mass?: number; // Mass of the spring (default: 1)
damping?: number; // Additional damping (default: 0)
stiffness?: number; // Alternative to tension (default: 100)
}
import { springPresets } from 'primotion';
// Available presets
springPresets.gentle // { tension: 120, friction: 14 }
springPresets.wobbly // { tension: 180, friction: 12 }
springPresets.stiff // { tension: 210, friction: 20 }
springPresets.slow // { tension: 40, friction: 10 }
springPresets.default // { tension: 170, friction: 26 }
import React, { useState } from 'react';
import { useSpring } from 'primotion';
function Counter() {
const [count, setCount] = useState(0);
const { value, animate } = useSpring({ from: 0, to: count });
const increment = () => {
setCount(prev => prev + 1);
animate(count + 1);
};
return (
<div>
<h1>{Math.round(value)}</h1>
<button onClick={increment}>Increment</button>
</div>
);
}
import React from 'react';
import { FadeIn } from 'primotion';
function StaggeredList() {
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
return (
<div>
{items.map((item, index) => (
<FadeIn key={item} delay={index * 100}>
<div style={{ padding: '10px', margin: '5px', background: '#f0f0f0' }}>
{item}
</div>
</FadeIn>
))}
</div>
);
}
import React from 'react';
import { useSpring } from 'primotion';
function CustomAnimation() {
const { value, animate } = useSpring({
from: 0,
to: 1,
config: { tension: 200, friction: 15 },
immediate: true,
});
return (
<div>
<div
style={{
width: '100px',
height: '100px',
background: 'linear-gradient(45deg, #ff6b6b, #4ecdc4)',
borderRadius: '50%',
transform: `scale(${value})`,
transition: 'none',
}}
/>
<button onClick={() => animate(value === 1 ? 0.5 : 1)}>
Toggle Scale
</button>
</div>
);
}
import React, { useState } from 'react';
import { useSpring } from 'primotion';
function AnimatedList() {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const items = ['Apple', 'Banana', 'Cherry', 'Date'];
return (
<div>
{items.map((item, index) => {
const { value, animate } = useSpring({
from: 0,
to: hoveredIndex === index ? 1 : 0,
config: { tension: 150, friction: 20 },
});
return (
<div
key={item}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
style={{
padding: '15px',
margin: '5px',
background: '#f8f9fa',
borderRadius: '8px',
transform: `translateX(${value * 20}px)`,
transition: 'none',
}}
>
{item}
</div>
);
})}
</div>
);
}
import React from 'react';
import { useSpring } from 'primotion';
function LoadingSpinner() {
const { value } = useSpring({
from: 0,
to: 360,
config: { tension: 100, friction: 20 },
immediate: true,
});
return (
<div
style={{
width: '40px',
height: '40px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #007bff',
borderRadius: '50%',
transform: `rotate(${value}deg)`,
}}
/>
);
}
import React, { useState } from 'react';
import { useSpring } from 'primotion';
function ProgressBar() {
const [progress, setProgress] = useState(0);
const { value, animate } = useSpring({
from: 0,
to: progress,
config: { tension: 150, friction: 20 },
});
const updateProgress = (newProgress: number) => {
setProgress(newProgress);
animate(newProgress);
};
return (
<div>
<div
style={{
width: '300px',
height: '20px',
background: '#f0f0f0',
borderRadius: '10px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${value}%`,
height: '100%',
background: '#007bff',
transition: 'none',
}}
/>
</div>
<button onClick={() => updateProgress(25)}>25%</button>
<button onClick={() => updateProgress(50)}>50%</button>
<button onClick={() => updateProgress(75)}>75%</button>
<button onClick={() => updateProgress(100)}>100%</button>
</div>
);
}
import React, { useState } from 'react';
import { useSpring } from 'primotion';
function AnimatedCard() {
const [isFlipped, setIsFlipped] = useState(false);
const { value, animate } = useSpring({
from: 0,
to: isFlipped ? 180 : 0,
config: { tension: 200, friction: 15 },
});
return (
<div
style={{
perspective: '1000px',
width: '200px',
height: '300px',
}}
>
<div
style={{
width: '100%',
height: '100%',
transform: `rotateY(${value}deg)`,
transformStyle: 'preserve-3d',
transition: 'none',
}}
>
{/* Front side */}
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
background: '#007bff',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
backfaceVisibility: 'hidden',
}}
>
Front
</div>
{/* Back side */}
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
background: '#28a745',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '8px',
transform: 'rotateY(180deg)',
backfaceVisibility: 'hidden',
}}
>
Back
</div>
</div>
<button
onClick={() => {
setIsFlipped(!isFlipped);
animate(isFlipped ? 0 : 180);
}}
style={{ marginTop: '10px' }}
>
Flip Card
</button>
</div>
);
}
- Use
immediate: false
for animations that should start on user interaction - Batch animations using delays for staggered effects
- Clean up animations by calling
stop()
in useEffect cleanup - Use
requestAnimationFrame
(already handled by the library) - Avoid animating layout-heavy properties like
width
andheight
- Use transform properties for better performance
- React 16.8+ (for hooks support)
- Modern browsers with
requestAnimationFrame
support - TypeScript 4.0+ (for type definitions)
MIT License - see LICENSE file for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
- Initial release
- Core spring animation engine
useSpring
hookSpring
,FadeIn
,SlideIn
components- TypeScript support
- Preset configurations