PrimoDev/ apps

Primotion

Animações de mola pra React, sem dependências além do React

  • Web
  • Grátis
  • Open source
Ver no GitHub
npm

O que é o Primotion?

O Primotion é uma biblioteca leve pra React que anima qualquer elemento com física de mola. Ele traz o hook useSpring e os componentes Spring, Animated, FadeIn e SlideIn, tudo rodando em requestAnimationFrame.

O package.json e o npm declaram licença MIT, mas o repositório não tem arquivo LICENSE. Instala com npm install primotion (também funciona com yarn e pnpm).

O que ele faz

  1. 01Hook useSpring com value, animate, setValue, stop e isAnimating
  2. 02Componentes Spring, Animated, FadeIn e SlideIn
  3. 03SlideIn em quatro direções com distância configurável
  4. 04Presets de mola: gentle, wobbly, stiff, slow e default
  5. 05Configuração de tension, friction e mass
  6. 06Callbacks onUpdate e onComplete e delay pra animações escalonadas
  7. 07Sem dependências externas, só React como peer
  8. 08Tipos TypeScript incluídos

Perguntas frequentes

Como instalo o Primotion?
npm install primotion, yarn add primotion ou pnpm add primotion. A última versão no npm é a 1.0.3.
Com quais versões do React funciona?
As peer dependencies são react >= 16.8 e react-dom >= 16.8, porque a biblioteca usa hooks. É pra React na web, não pra React Native.
Qual é a licença?
O package.json e o npm declaram MIT. O repositório ainda não tem um arquivo LICENSE.
Qual a diferença pro react-spring ou Framer Motion?
O Primotion é bem menor: um motor de mola próprio, um hook e quatro componentes, sem dependências além do React. Os presets usam a mesma ideia de tension e friction do react-spring, mas não tem gestos, layout animations nem o resto do ecossistema dessas bibliotecas.
O projeto está ativo?
Foi publicado em julho de 2025 e não teve commits desde então. O repositório tem uma landing page de demo em Vite que roda localmente.

Documentação

README do repositório lucianodiisouza/primotion, atualizado a cada deploy. O texto original é em inglês.

A lightweight, performant React library for adding smooth spring animations to any element. Built with TypeScript and optimized for modern React applications.

npm version License: MIT TypeScript

✨ Features

  • 🎯 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

📦 Installation

npm install primotion
# or
yarn add primotion
# or
pnpm add primotion

🚀 Quick Start

Using the useSpring Hook

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>
  );
}

Using Pre-built Components

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>
  );
}

Using the Spring Component

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>
  );
}

📚 API Reference

useSpring(options)

A React hook that provides spring animation functionality.

Options

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

Returns

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

Spring Component

A component that wraps elements with spring animation capabilities.

Props

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

FadeIn Component

A component for fade-in animations.

Props

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

SlideIn Component

A component for slide-in animations.

Props

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

⚙️ Spring Configuration

SpringConfig Interface

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

Preset Configurations

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 }

🎯 Examples

Animated Counter

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>
  );
}

Staggered Animations

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>
  );
}

Custom Spring Animation

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>
  );
}

Interactive Hover Effects

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>
  );
}

Loading Spinner

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)`,
      }}
    />
  );
}

Progress Bar

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>
  );
}

Card Flip Animation

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>
  );
}

🚀 Performance Tips

  1. Use immediate: false for animations that should start on user interaction
  2. Batch animations using delays for staggered effects
  3. Clean up animations by calling stop() in useEffect cleanup
  4. Use requestAnimationFrame (already handled by the library)
  5. Avoid animating layout-heavy properties like width and height
  6. Use transform properties for better performance

🌐 Browser Support

  • React 16.8+ (for hooks support)
  • Modern browsers with requestAnimationFrame support
  • TypeScript 4.0+ (for type definitions)

📄 License

MIT License - see LICENSE file for details.

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 Changelog

1.0.1

  • Initial release
  • Core spring animation engine
  • useSpring hook
  • Spring, FadeIn, SlideIn components
  • TypeScript support
  • Preset configurations