Ultimate Scroll Animation Library

@usal/react npm package @usal/solid npm package @usal/svelte npm package @usal/vue npm package @usal/lit npm package @usal/angular npm package
Baseline|Baseline
~8KB
Gzipped Size
120FPS
Performance
Zero
Dependencies
40+
Animations
WAAPI
rAF
Engine

Quick Start

CDN

<script src="https://cdn.usal.dev/latest"></script>

NPM

# Install
npm install usal

# Framework-specific packages:
npm install @usal/react
npm install @usal/solid
npm install @usal/svelte
npm install @usal/vue
npm install @usal/lit
npm install @usal/angular

Framework Setup

// Installed via CDN, initializes automatically, instance in window.USAL

// React (Next.js)
import { USALProvider } from '@usal/react';
<USALProvider>{children}</USALProvider>

// Solid (SolidStart)
import { USALProvider } from '@usal/solid';
<USALProvider>{props.children}</USALProvider>

// Svelte (SvelteKit)
import { usal } from '@usal/svelte';
// USAL auto-initializes globally

// Vue (Nuxt)
import { USALPlugin } from '@usal/vue';
createApp(App).use(USALPlugin).mount('#app');
//for Nuxt
export default defineNuxtConfig({
modules: ['@usal/vue/nuxt']
//...

// Lit
import { usal } from '@usal/lit';
// USAL auto-initializes globally

// Angular
import { USALModule } from '@usal/angular';
@Component({imports: [USALModule]})
export class AppComponent
See how simple it is to use
Main animation attribute
<!-- Vanilla JS -->
<div data-usal="fade duration-500">Content</div>

<!-- React/Next.js -->
<div data-usal="fade duration-500">Content</div>

<!-- Solid/SolidStart -->
<div data-usal="fade duration-500">Content</div>

<!-- Svelte/SvelteKit -->
<div use:usal={'fade duration-500'}>Content</div>

<!-- Vue/Nuxt -->
<div v-usal="'fade duration-500'">Content</div>

<!-- Lit -->
<div ${usal('fade duration-500')}>Content</div>

<!-- Angular -->
<div usal="fade duration-500">Content</div>

Basic Animations

Fade Up
fade
Fade Up
fade-u
Fade Down
fade-d
Fade Left
fade-l
Fade Right
fade-r
Fade Up-Left
fade-ul
Fade Up-Right
fade-ur
Fade Down-Left
fade-dl
Fade Down-Right
fade-dr
Slide Up
slide
Slide Up
slide-u
Slide Down
slide-d
Slide Left
slide-l
Slide Right
slide-r
Slide Up-Left
slide-ul
Slide Up-Right
slide-ur
Slide Down-Left
slide-dl
Slide Down-Right
slide-dr
<div data-usal="fade-u">Fade Up</div>
<div data-usal="slide-dr duration-800">Slide Down-Right</div>
<div data-usal="fade-l delay-200">Fade Left with Delay</div>
<div data-usal="slide-r forwards">Slide Right (stays)</div>

Zoom Animations

ZoomIn Up
zoomin
ZoomIn Up
zoomin-u
ZoomIn Down
zoomin-d
ZoomIn Left
zoomin-l
ZoomIn Right
zoomin-r
ZoomIn Up-Left
zoomin-ul
ZoomIn Up-Right
zoomin-ur
ZoomIn Down-Left
zoomin-dl
ZoomIn Down-Right
zoomin-dr
ZoomOut Up
zoomout
ZoomOut Up
zoomout-u
ZoomOut Down
zoomout-d
ZoomOut Left
zoomout-l
ZoomOut Right
zoomout-r
ZoomOut Up-Left
zoomout-ul
ZoomOut Up-Right
zoomout-ur
ZoomOut Down-Left
zoomout-dl
ZoomOut Down-Right
zoomout-dr
<div data-usal="zoomin">Scale from 60% to 100%</div>
<div data-usal="zoomout-ur blur duration-1200">Complex zoom</div>

Flip Animations

Flip Default
flip
Flip Up
flip-u
Flip Down
flip-d
Flip Left
flip-l
Flip Right
flip-r
Flip Up-Left
flip-ul
Flip Up-Right
flip-ur
Flip Down-Left
flip-dl
Flip Down-Right
flip-dr
<div data-usal="flip-u">3D flip from top</div>
<div data-usal="flip-dr duration-1500">Diagonal flip</div>

Text Split Animations

Each Word Appears Separately

Each Letter Appears Separately

Zoom Out With Blur Per Word

Modern web development is all about smooth user experiences. Dream big, code bigger, animate everything in between.

Fluid Weight Animation

SHIMMER LETTER BY LETTER

<p data-usal="fade-u split-word split-delay-200">Each Word</p>
<p data-usal="fade-u split-letter split-delay-50">Each Letter</p>
<h2 data-usal="text-fluid split-letter duration-2000 split-delay-50">Fluid Weight</h2>
<p data-usal="text-shimmer split-letter duration-2000 split-delay-100">Shimmer</p>

Count Animations

1234
Users
98.5%
Success Rate
$42,350
Revenue
4.9/5
Rating
<div data-usal="count-[1234] duration-4000">1234</div>
<div data-usal="count-[98.5] duration-4000">98.5%</div>

Modifiers & Options

Once (no repeat)
fade-u once
Linear easing
zoomin linear
Ease easing
flip-r ease
Ease-in easing
fade-d ease-in
Ease-out easing
zoomout duration-1000 easing-[steps(4)]
2s + Blur
fade-ul blur duration-2000
1s delay
flip-d delay-1000
50% threshold
zoomin-r threshold-50
<div data-usal="fade-u once">Animate only once</div>
<div data-usal="zoomin linear">Linear easing</div>
<div data-usal="fade-ul blur duration-2000">With blur</div>

Cards & Timing

Instant Animation

fade-u delay-0 duration-800

200ms Delay

zoomin delay-200 duration-1000

400ms Delay

flip-r delay-400 duration-1200

600ms Delay + Blur

fade-ur delay-600 duration-1500 blur

Split Items Animation

  • Item A
  • Item B
  • Item C
  • Item D
  • Item E
  • Item F
  • Item G
  • Item H
<ul data-usal="split-item split-fade-r split-delay-100">
  <li>First item</li>
  <li>Second item</li>
  <li>Third item</li>
</ul>

Advanced Timeline

Loop Animation
fade-u loop
Forwards State
line-[|50s+1.5o+50] forwards
Custom Blur
flip-r blur-3
Tuned Movement
fade-40
Tuned Zoom
zoomin-30-50
Timeline Basic
line-[o+0 s+0.5 | 50 s+1.2 | o+100 s+1]
Timeline 3D
line-[p+150 ry+0 | 50 ry+180 | ry+360]
Timeline Blur
line-[b+5 o+0 | 30 b+2 o+50 | b+0 o+100]
Bang!
<!-- Loop Animation -->
<div data-usal="fade-u loop">Continuous loop</div>

<!-- Forwards State -->
<div data-usal="line-[|50s+1.5o+50] forwards">Keep final state</div>

<!-- Custom Blur -->
<div data-usal="flip-r blur-3">3rem blur effect</div>

<!-- Animation Tuning -->
<div data-usal="fade-40">40% movement distance</div>
<div data-usal="zoomin-30-50">30% movement, 50% intensity</div>
<div data-usal="flip-120-60">120° angle, 60rem perspective</div>

<!-- Timeline Animations -->
<div data-usal="line-[o+0 s+0.5 | 50 s+1.2 | o+100 s+1] duration-2000">
  Multi-keyframe timeline
</div>

<div data-usal="line-[p+150 ry+0 | 50 ry+180 | ry+360] duration-3000">
  3D rotation timeline
</div>

<div data-usal="line-[
  p+150 ry+0
  | 20 ry+1440
  | 50 ry+2160
  | 80 ry+2340
  | 95 ry+2430 rx+30
  | ry+2440 rx+90
] duration-5000 forwards">
  Bang!
</div>

Dynamic Content Test

// Add items dynamically
const item = document.createElement('div');
item.textContent = 'New Item';
item.setAttribute('data-usal', 'fade-u duration-800');
parent.appendChild(item);

API Documentation

Property Values

Available Animations
Core animation types
// Basic animations
fade, fade-u, fade-d, fade-l, fade-r
fade-ul, fade-ur, fade-dl, fade-dr

// Slide animations
slide, slide-u, slide-d, slide-l, slide-r
slide-ul, slide-ur, slide-dl, slide-dr

// Zoom in animations
zoomin, zoomin-u, zoomin-d, zoomin-l, zoomin-r
zoomin-ul, zoomin-ur, zoomin-dl, zoomin-dr

// Zoom out animations
zoomout, zoomout-u, zoomout-d, zoomout-l, zoomout-r
zoomout-ul, zoomout-ur, zoomout-dl, zoomout-dr

// Flip animations
flip, flip-u, flip-d, flip-l, flip-r
flip-ul, flip-ur, flip-dl, flip-dr
Easing Functions
Control animation acceleration
// Preset easing
linear, ease, ease-in, ease-out, ease-in-out, step-start, step-end

easing-[{value}] // Custom easing: easing-[cubic-bezier(0.4,0,0.2,1)]
// Other custom possibilities
linear(0, 0.25, 1)
steps(4, end)
Split Animations
Animate text parts individually
// Split by words
split-word

// Split by letters
split-letter

// Split by child items
split-item

// Split with custom animation
split-fade-r, split-fade-u, split-zoomin

// Split delay in milliseconds
split-delay-50, split-delay-100

// Split delay with stagger types
split-delay-50-linear
split-delay-50-center  // X/Y axes
split-delay-50-edges   // X/Y axes
split-delay-50-random
Count Animation
Animate numbers from 0 to their displayed value
// Syntax: count-[target]
// The target number must exist in the element's text content

count-[1234]     // Element must contain "1234"
count-[98.5]     // Element must contain "98.5"  
count-[42,350]   // Element must contain "42,350"

// HTML Example:
<span data-usal="count-[1000]">1000</span>
// Animates: 0 → 1000

<span data-usal="count-[4.9]">Rating: 4.9/5</span>
// Finds "4.9" and animates: 0 → 4.9
Modifiers
Animation behavior modifiers
// Duration in milliseconds
duration-500, duration-1000, duration-2000

// Delay in milliseconds
delay-200, delay-500, delay-1000

// Threshold (percentage of element visible)
threshold-10, threshold-30, threshold-50

// Blur effect
blur           // Default blur (0.625rem)
blur-2         // Custom blur (2rem)

// Behavior modifiers
once           // Run animation only once
loop / loop-{mirror/jump}   // Run animation continuously (overrides 'once') (default: mirror)
forwards       // Keep final animation state
Text Effects
Special character animations (requires split-letter)
// Effects available:
text-shimmer    // Shimmering light sweep
text-fluid      // Morphing font weight

// Must be used with split-letter:
<h1 data-usal="split-letter text-shimmer">Text</h1>

// For best results:
// - Use duration-2000 to duration-3000
// - Use split-delay-50 to split-delay-100
<h1 data-usal="split-letter text-shimmer duration-2000 split-delay-75">
  Beautiful
</h1>
Tip: For large elements (>100vh), use lower thresholds like threshold-5 or threshold-10 to ensure animations trigger properly.

Advanced

data-usal-id
Unique identifiers control animation reactivity
<!-- Automatic ID (re-animates when element is recreated) -->
<div data-usal="fade-u">Auto ID</div>

<!-- Custom ID (prevents re-animation) -->
<div data-usal="fade-u" data-usal-id="unique-element">Fixed ID</div>
Animation Tuning
Fine-tune animations with numeric values
// Syntax: {animation}-{direction}-{value1}-{value2}-{value3}

// Fade: controls movement distance
fade-40         // 40% movement (default: 15% normal, 50% split(word/letter))
fade-30-60      // X: 30%, Y: 60%

// Zoom: controls scale and movement
zoomin-30       // 30% intensity (default: 15% normal, 50% split(word/letter))
zoomin-40-60    // movement: 40%, intensity: 60%
zoomin-30-50-80 // X: 30%, Y: 50%, intensity: 80%

// Flip: controls rotation and perspective
flip-120        // 120° angle (default: 90°)
flip-90-60      // angle: 90°, perspective: 60rem (default: 25rem)
Custom Timeline
Create precise keyframe animations
// Syntax: line-[{timeline}]
// Case-insensitive, spaces/linebreaks allowed for readability

// PROPERTIES REFERENCE:
// o±value  = Opacity (0-100, auto-clamped to 0-1)
// s±value  = Scale (X and Y axes)
// sx/sy/sz = Scale individual axes
// t±value  = Translate (X only, %)
// tx/ty/tz = Translate individual axes (%)
// r±value  = Rotate (Z-axis, degrees)
// rx/ry/rz = Rotate individual axes (degrees)
// b±value  = Blur (rem, negatives become 0)
// g±value  = Glow (brightness) (%, negatives become 0)
// w±value  = Weight (font weight) (100-900, negatives become 0)
// p±value  = Perspective (rem)

// KEYFRAME BEHAVIORS:

// No pipes: FROM value TO original state
line-[o+0]                    // Opacity: 0 → 1 (original)
line-[s+0.5]                  // Scale: 0.5 → 1 (original)
line-[o+0 s+0.3 tx+50]        // Multiple: all start from values → original

// One pipe: FROM original TO value
line-[|o+50]                  // Opacity: 1 → 0.5
line-[|60s+1.5]               // At 60%: scale to 1.5, holds to 100%
line-[|o+20 s+1.5]            // Multiple: original → final values

// Multiple pipes (2+): Copies first to 0%, last to 100%
line-[|25o+30|50o+60|75o+90]
// 0%: o+30, 25%: o+30, 50%: o+60, 75%: o+90, 100%: o+90

// COMPLEX EXAMPLES:

// Multi-property animation
line-[o+0 s+0.5 tx+50 | 50 s+1.2 tx+25 | o+100 s+1 tx+0]
// 0%: opacity 0, scale 0.5, translateX 50%
// 50%: scale 1.2, translateX 25%
// 100%: opacity 1, scale 1, translateX 0

// 3D transformations (order matters!)
line-[rx+90 p+50]             // Flip X with perspective
line-[p+100 ry+180 sx+0.5]    // Perspective first, then rotate & scale

// Multi-line for readability
line-[
  o+0 sx+0.2 sy+0.2 tx+70 |
  40 o+50 sx+0.8 sy+0.8 tx+35 |
  70 o+90 sx+0.95 sy+0.95 tx+5 |
  o+100 sx+1 sy+1 tx+0
]

// Fluid Text
line-[w+100|50w+900|w+100] loop linear split-letter

// Shimmer Text
line-[o+50g+100|50o+100g+130|o+50g+100] loop linear split-letter

// Combine with modifiers
line-[o+0s+0.5] duration-2000 forwards
line-[|50s+1.5] loop
line-[rx+360] easing-[cubic-bezier(0.4,0,0.2,1)]

// IMPORTANT NOTES:
// - Transform order affects result (rotate then translate ≠ translate then rotate)
// - Don't mix general & specific (s+0.5 with sx+0.8 = conflict)
// - Duplicate properties: last value wins
// - Overrides standard animations (fade, zoomin, etc.)

JavaScript API

window.USAL.config(options)
Configure or reconfigure at any time
// Initial configuration
window.USAL.config({
  defaults: {
    animation: "fade",      // Default animation type
    direction: "u",         // Direction (u, d, l, r, ul, ur, dl, dr)
    duration: 1000,         // Animation duration (ms)
    delay: 0,               // Animation delay (ms)
    threshold: 10,          // Viewport threshold (%)
    splitDelay: 30,         // Delay between split items (ms)
    forwards: false,        // Keep final animation state
    easing: "ease-out",     // CSS easing function
    blur: false             // false, true (0.625rem), or number (rem)
    loop: 'mirror',         // Default loop type (mirror, jump)
  },
  observersDelay: 50,       // Delay for observers (ms)
  once: false               // Run animation only once
});

// Can be reconfigured later
window.USAL.config({
  defaults: {
    duration: 2000,         // Change duration
    easing: "ease-in-out"   // Change easing
  },
  once: true                // Now runs only once
});
window.USAL.destroy()
Completely shut down USAL, remove all observers and restore original state
// Completely shut down USAL
window.USAL.destroy();
// After destroy, USAL is turned off completely
// Elements return to their original state
window.USAL.restart()
Destroy and reinitialize USAL (equivalent to destroy + init)
// Restart USAL completely
window.USAL.restart();
// This is the same as calling:
// window.USAL.destroy() then reinitializing
Properties
Read-only properties
// Check if initialized
console.log(window.USAL.initialized()); // returns boolean

// Get version
console.log(window.USAL.version); // string

Framework Comparison

Changelog

v1.3.1

October 2025

Bug Fixes & Stability

  • Blazor Component Reuse Compatibility: Fixed edge case where Blazor's DOM element reuse caused animations to trigger on elements without data-usal attributes
    • USAL now ignores elements with empty or whitespace-only data-usal values
    • Prevents animations from triggering when Blazor strips attributes during navigation
    • Special thanks to @mdmontesinos for the detailed investigation and testing (Issue #5)
  • Tab Visibility Desynchronization: Fixed stagger timing issues when browser tabs become inactive
    • Implemented virtual time system capped at 16.67ms per frame
    • Stagger delays now remain synchronized after tab switches
    • Eliminates animation "bunching" when returning to inactive tabs
    • Applies to both split animations and count animations
  • Console Error Handling: Improved error handling for replaceChild operations during DOM manipulation
    • Elements causing errors are now properly ignored and cleaned up
    • No visual impact on animations
    • Reduces console spam in edge cases
  • Split Animation Nesting: Fixed excessive DOM nesting in split letter/word animations
    • Proper instance detection prevents duplicate wrapper generation
    • Cleaner DOM structure with minimal nesting

v1.3.0

September 2025

Brand Identity & Visual Refresh

  • New Logo System: Complete rebrand with automatic dark/light theme switching
  • Full icon set in multiple formats (PNG, WebP, GIF) and sizes (16px to 512px)
  • Updated favicon and social media assets for better brand consistency

SSR & Framework Compatibility

  • Complete SSR Overhaul: HTML structure now fully preserved during animations
  • No element cloning or reconstruction - only text nodes modified for text/count animations
  • Angular Fix: Migrated to standalone directive with proper usal="value" syntax
  • Added Vue.js plugin setup alongside existing Nuxt configuration
  • Eliminated hydration mismatches across all SSR frameworks

Animation Engine Improvements

  • Fixed 5-95% Progress Bug: Animations now use full 0-100% range, eliminating visual artifacts
  • Text Effects via Timeline: text-shimmer and text-fluid now use timeline syntax
    • Shimmer: line-[o+50g+100|50o+100g+130|o+50g+100]
    • Fluid: line-[w+100|50w+900|w+100]
  • New timeline properties: g±value (glow/brightness) and w±value (font weight morphing)
  • Better synchronization with eliminated "snap" effect at animation boundaries

Testing & Documentation

  • Comprehensive Test Suites: Added integration tests for all major frameworks
  • Framework usage examples moved directly below installation for better discoverability
  • Updated API documentation with new timeline properties and examples

Robustness & Performance

  • Added processing locks to prevent race conditions during DOM mutations
  • Configuration System: Improved config.defaults inheritance and cascading
  • Optimized DOM manipulation with RequestAnimationFrame batching
  • Enhanced memory management and cleanup with proper promise chains

v1.2.3

September 2025

New Slide Animation

  • Pure movement animation with slide - no opacity changes
  • All directional variants supported (slide-u, slide-d, slide-l, slide-r, etc.)
  • Maintains original element opacity throughout animation
  • Perfect for elements that need to stay fully visible during entrance

Critical Bug Fixes

  • HTML Preservation: Split and count animations now correctly preserve HTML structure (bold, italic, nested elements)
  • Animation Persistence: Fixed parser bug that was resetting all animations to fade
  • Split Tuning: Tuning parameters like fade-u-200 now work correctly with split animations
  • Letter Animation: Fixed inline-block display and "snap" effect after animation completion

Performance & Compatibility

  • Improved animation cleanup with better garbage collection hints
  • More efficient memory management for long-running applications
  • Fixed Node.js version compatibility issues in build script
  • Works seamlessly across Node.js versions 18-24

Internal Improvements

  • Refactored configuration generation with genEmptyConfig()
  • Clearer variable naming throughout the codebase
  • Better separation of concerns in animation setup functions
  • Enhanced recursive processing for DOM manipulation

v1.2.2

September 2025

Advanced Loop Controls

  • New loop-mirror and loop-jump modifiers for different loop behaviors
  • Back and forth animations with loop-mirror (default behavior)
  • Restart animations from beginning with loop-jump
  • Configurable default loop type via config.defaults.loop

Enhanced Split Animation Stagger

  • Linear distance-based stagger with split-delay-{value}-linear
  • Center-outward stagger on X/Y axes with split-delay-{value}-center
  • Edges-inward stagger on X/Y axes with split-delay-{value}-edges
  • Random stagger pattern with split-delay-{value}-random

Precision & Performance

  • Support for decimal blur values (e.g., blur-0.5, blur-1.5)
  • Added ease-in-out, step-start, and step-end easing functions
  • Refactored animation controller with better state management
  • Improved parsing for split animations with tuning parameters

Playground Enhancements

  • Enhanced controls with loop type selector and stagger pattern options
  • New preset examples demonstrating stagger effects
  • Updated debug panel with loop and stagger test cases

v1.2.1

September 2025

Bug Fixes

  • Fixed animation tuning values parsing when no direction is specified (e.g., fade-50, flip-90 now work correctly)

v1.2.0

September 2025

Advanced Animation Features

  • Loop animations: New loop modifier for continuous animation cycles
  • Custom timeline animations: Advanced line-[{timeline}] syntax for precise keyframe control
  • Animation tuning: Numeric parameters for fine-tuning standard animations (fade-40, zoomin-30-50, flip-120-60)
  • Enhanced blur effects: Custom blur intensity with blur-{value} syntax
  • Forwards modifier: forwards option to maintain final animation state

Timeline System

  • Support for opacity, scale, translate, rotate, blur, and perspective properties
  • Multi-keyframe animations with percentage-based timing
  • 3D transformations with proper transform order handling
  • Case-insensitive syntax with multi-line support for readability

Technical Updates

  • Bundle size updated to ~8KB gzipped (from ~5KB) due to advanced features
  • Enhanced debug panel with comprehensive testing suite
  • Improved element cleanup and animation state handling

v1.1.1

September 2025

Robustness & Stability

  • Complete API overhaul with promise-based chain system (Rust-like enum states)
  • Significantly improved edge case handling for extreme usage scenarios
  • Better orchestrated state machine for animation lifecycle
  • Enhanced public API usability and consistency

Bug Fixes

  • Fixed text animations losing characteristics in flex containers without wrapper
  • Resolved smooth scroll issue on mobile when switching tabs

Developer Experience

  • Added comprehensive debug panel in source code for development testing
  • Improved error handling and recovery mechanisms

v1.1.0

September 2025

Complete WAAPI Migration

  • Full migration to Web Animations API (WAAPI) for all animations
  • Only count animations remain using RAF for precise number formatting
  • Zero direct DOM manipulation - no inline styles or attributes

Framework Compatibility

  • Eliminated SSR hydration mismatches
  • No longer manipulates HTML node attributes directly
  • Perfect compatibility with React, Vue, Svelte, Solid, and other SSR frameworks

Performance

  • Significantly improved performance through WAAPI
  • Better browser optimization and hardware acceleration
  • Reduced JavaScript overhead

v1.0.0

September 2025

Initial Release

  • 40+ scroll-triggered animations
  • Intersection Observer based triggers
  • Zero dependencies, ~5KB gzipped
  • Full Shadow DOM support
  • Framework packages for all major libraries
  • Automatic initialization
  • Split text animations (word/letter)
  • Count animations with smart formatting