tween
ComponentsTemplatesShowcaseContact

Command Palette

Search for a command to run...

Getting Started
  • Introduction
  • Installation
  • StoryNew
Guides
  • Motion Principles
  • Scroll and Smooth Scroll
  • Performance and Accessibility
Scroll Animations
  • Sticky Cards Scroll
  • Rise Fan Cards Scroll
  • Rotate Stack Scroll
  • Doodle Lens ScrollNew
  • Slit Open Hero ScrollNew
  • Logo Mask Zoom ScrollNew
  • ASCII Dissolve ScrollNew
  • Lottie Flight ScrollNew
  • Inline Icon Text ScrollNew
  • Starfield ScrollNew
  • Name Swap ScrollNew
  • Spotlight Gallery HeroNew
  • Slit Rotate Hero ScrollNew
  • Stroke Cover ScrollNew
  • Stroke Rows ScrollNew
Text Animations
  • Text Skew Reveal
  • Block Text Reveal
Hover & Cursor
  • Image Trail Cursor
  • Cursor Depth Logo
  • Physics Tag Drop
  • Elastic Card ListNew
  • Magnetic Cards SpringNew
Sliders & Carousels
  • Wheel Fullscreen Carousel
  • Ripple Displacement Slider
  • Three.js Blend Slider
  • Stacked Deck Slider
  • Orbit Cylinder SliderNew
  • Fan Stack SliderNew
  • Exponential Strip SliderNew
  • Dual Column SliderNew
Galleries & Grids
  • Scroll Wave Gallery
Menus & Navigation
  • Shrink Page MenuNew
Preloaders & Intros
  • Floating Items RevealNew
  • Word Cycle RevealNew
  • Tile Grid LoaderNew
Page Transitions
  • Panel Assembly TransitionNew
3D & WebGL
  • Fractal Glass Hero
  • Fluid Cursor Canvas
  • Fluid Portrait Hero
  • Fluid Gradient Canvas
  • Inversion Lens HoverNew
UI Elements
  • Circular Widgets
  • Vinyl Cover
  • Image Explosion Footer
  • Accordion Image FramesNew

(Tween) — Motion registry

Animated components & full sites, one command away.

Product

  • Components
  • Templates
  • Installation
  • Guides
  • Blog
  • Contact

Categories

  • Scroll Animations
  • Text Animations
  • Hover & Cursor
  • Sliders & Carousels
  • Galleries & Grids
  • Menus & Navigation
  • Preloaders & Intros
  • Page Transitions
  • 3D & WebGL
  • UI Elements
TWEEN
ease: power2.inOut0.00s
© 2026 TweenBack to topBuilt by mohammad
  1. Docs/
  2. Guides/
  3. Performance and Accessibility

Performance and Accessibility

PreviousNext

Rules for motion that stays fast, cleans up after itself and works for everyone.

Rich motion is only worth shipping if it holds its frame rate on an ordinary phone and never locks anyone out. These are the rules Tween components follow, and the ones to keep when you adapt them.

Common mistakes

Motion code copied from demos tends to fail in the same places. Check for these first:

  • No prefers-reduced-motion handling at all.
  • Hover effects with no touch fallback and no keyboard equivalent.
  • Menus without a real <button>, Escape key, focus management or scroll lock.
  • Page transitions that never move focus or announce the new page.
  • Canvases that keep rendering off-screen and are never disposed.
  • Preloaders that ignore real loading, cannot be skipped and replay on every visit.
  • Per-frame damping without delta time, so followers run twice as fast on 120 Hz screens.
  • Tweening width, height, top, left or gap; creating new tweens every frame; global selectors inside React components.
  • user-select: none on text, empty alt on content images, and multi-megabyte unoptimised images and models.

Reduced motion

Check the preference inside the effect and show the final composition when motion is reduced. The reduced version may use short fades (≤ 0.3 s), but no counters, no image cycling and no idle canvas animation.

React.useEffect(() => {
  const root = ref.current
  if (!root) return
 
  const reduceMotion =
    typeof window !== "undefined" &&
    window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
 
  if (reduceMotion) {
    gsap.set(root.querySelectorAll("[data-line]"), { yPercent: 0 })
    return
  }
 
  // ...full animation
}, [])

Make sure the reduced branch still lands on a complete, readable layout. A section that was meant to reveal content on scroll must not leave it hidden.

Clean up everything

Each effect returns a cleanup that undoes all of its work. Tween components use plain useEffect with either a scoped gsap.context or explicit kills:

React.useEffect(() => {
  const root = containerRef.current
  if (!root) return
 
  const ctx = gsap.context(() => {
    // tweens and ScrollTriggers created here are recorded
  }, root)
 
  const onResize = () => ScrollTrigger.refresh()
  window.addEventListener("resize", onResize)
 
  return () => {
    window.removeEventListener("resize", onResize)
    ctx.revert() // kills tweens, triggers and pins, restores inline styles
  }
}, [activeScroller])

The cleanup has to cover every kind of side effect: tweens and timelines, ScrollTriggers, SplitText instances (split.revert()), event listeners, gsap.ticker callbacks, requestAnimationFrame loops, observers, and WebGL resources.

Scope selectors to the component root (root.querySelectorAll(...), or the gsap.context scope) so two copies of the same component on one page never animate each other's elements.

Pointer, touch and keyboard

Listen on the container

Attach pointer, wheel and touch listeners to the component's own element, not to window or document, and remove them in cleanup. That keeps a hover effect from reacting while the pointer is over a different section, and it makes the component safe to render twice.

  • Mark listeners { passive: true } unless the component genuinely owns the gesture (a draggable slider, for example) and has to call preventDefault().
  • Never block touchmove for a decorative effect. Use CSS touch-action to declare which gestures the element handles.

Gate hover effects

Pointer-follow and hover effects run only on devices that can hover with a precise pointer. On touch devices, show a static or auto-playing version, never a blank space.

const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches

Focus parity

Every interactive row, card or link is a real <a href> or <button>, and keyboard focus runs the same enter and leave code as the pointer:

React.useEffect(() => {
  const root = listRef.current
  if (!root) return
 
  const items = Array.from(root.querySelectorAll<HTMLElement>("[data-item]"))
  const cleanups = items.map((item) => {
    const enter = () =>
      gsap.to(item, { x: 16, duration: 0.5, ease: "power2.out" })
    const leave = () =>
      gsap.to(item, { x: 0, duration: 0.5, ease: "power2.out" })
 
    item.addEventListener("pointerenter", enter)
    item.addEventListener("pointerleave", leave)
    item.addEventListener("focusin", enter)
    item.addEventListener("focusout", leave)
 
    return () => {
      item.removeEventListener("pointerenter", enter)
      item.removeEventListener("pointerleave", leave)
      item.removeEventListener("focusin", enter)
      item.removeEventListener("focusout", leave)
      gsap.killTweensOf(item)
    }
  })
 
  return () => cleanups.forEach((fn) => fn())
}, [])

See Hover Preview List and Image Trail Cursor for complete hover components.

Animate cheap properties

  • Animate transform, opacity and clip-path. Use masks sparingly. Do not tween top, left, width or height; move or scale the element instead, or use the Flip plugin to animate between layouts.
  • For anything that follows the pointer, create the setter once with gsap.quickTo or gsap.quickSetter and call it on each move. Never create a new tween per pointermove.
  • Add will-change: transform only to elements that are actually animating, and only for as long as they animate.

Frame-rate independent damping

A follower written as x += (target - x) * 0.1 moves twice as fast at 120 Hz as at 60 Hz. Scale the factor by elapsed time:

const rate = 8 // higher is snappier
let x = 0
let targetX = 0
 
const tick = (_time: number, deltaMs: number) => {
  const k = 1 - Math.exp(-rate * (deltaMs / 1000))
  x += (targetX - x) * k
  gsap.set(follower, { x })
}
 
gsap.ticker.add(tick)
// cleanup: gsap.ticker.remove(tick)

Canvas and WebGL

Treat every canvas as a resource with a lifecycle:

  1. Lazy start. Create the renderer when the section approaches the viewport, not on page load.
  2. Pause when hidden. Stop the render loop with an IntersectionObserver when the canvas leaves the screen, and on visibilitychange when the tab is hidden. When resuming, account for the paused time so animations do not jump.
  3. Cap the pixel ratio. Math.min(window.devicePixelRatio, 2), or 1.5 on coarse pointers.
  4. Fail softly. Wrap context creation in try/catch, and keep a real <img alt="..."> underneath so the content survives without WebGL.
  5. Dispose on unmount. Geometries, materials, textures, render targets and the renderer itself.
  6. One live context at a time. Pause an off-screen canvas before the next one starts.
React.useEffect(() => {
  const canvas = canvasRef.current
  if (!canvas) return
 
  let visible = false
  let frame = 0
 
  const loop = () => {
    render()
    frame = requestAnimationFrame(loop)
  }
  const start = () => {
    if (!frame && visible && !document.hidden)
      frame = requestAnimationFrame(loop)
  }
  const stop = () => {
    cancelAnimationFrame(frame)
    frame = 0
  }
 
  const io = new IntersectionObserver(([entry]) => {
    visible = entry.isIntersecting
    visible ? start() : stop()
  })
  io.observe(canvas)
 
  const onVisibility = () => (document.hidden ? stop() : start())
  document.addEventListener("visibilitychange", onVisibility)
 
  return () => {
    stop()
    io.disconnect()
    document.removeEventListener("visibilitychange", onVisibility)
    disposeScene()
  }
}, [])

Canvas components such as Fractal Glass Hero, Fluid Cursor Canvas and Ripple Displacement Slider are good candidates for this treatment when you place several on one page.

Preloaders

  • Reach a readable hero in ≤ 2.5 s. Drive the preloader from real signals (document.fonts.ready and the hero image's decode()), with a 2.5 s ceiling and an error path.
  • Offer a focusable "Skip intro" button and close on Escape.
  • On later views in the same session, show a short version or none at all (store a flag in sessionStorage).
  • Remove the intro layer from the DOM when it finishes, and preload the hero image (next/image with priority) so it is the page's largest contentful paint.

See Loader Reveal Hero.

Menus

  • The toggle is a <button> with aria-expanded and aria-controls, and its label switches between "Menu" and "Close".
  • Trap focus inside the open menu, or mark the rest of the page inert. Mark the menu itself inert while closed.
  • Close on Escape and on route change. Stop smooth scroll while open.
  • Build the animation as a reversible paused timeline, or kill and rebuild it on each toggle, so rapid clicks never leave it half-open.

See Fullscreen Overlay Menu and Push Over Menu.

Route changes

  • Give <main> an id and tabindex="-1", and move focus to the new page's <h1> after the transition.
  • Announce the new page title in an aria-live="polite" region.
  • Mark the transition overlay aria-hidden and inert.
  • Keep one <h1> per page.
  • Back and forward navigation should either play the matching reverse transition or fall back to a simple crossfade.

Text

  • Split text must stay readable to screen readers. SplitText adds accessible labels to split elements; verify the output in your build with a screen reader or the accessibility tree.
  • Scramble, decode and flicker effects must never replace live text without an aria-label that carries the real string.
  • Do not set user-select: none on text. Give content images real alt text, and mark decorative clones and canvases aria-hidden.
  • Provide a skip link to the main content.

Assets

  • Load fonts with next/font, not a CSS @import.
  • Serve images through next/image with AVIF or WebP, sized to the largest on-screen size at 2× pixel density.
  • Compress 3D models (for example with gltf-transform, Meshopt or Draco geometry and KTX2 textures) to a few megabytes.
  • Encode video as H.264 plus AV1, muted, playsinline, with a poster frame.

Budgets

Per page, on a mid-range phone over 4G:

MetricTarget
Largest Contentful Paint≤ 2.5 s
Cumulative Layout Shift≤ 0.05
Interaction to Next Paint≤ 200 ms
Motion JavaScript before WebGL chunks, gzip≤ 120 KB
WebGL codeloaded on demand

Back to Motion Principles.

Scroll and Smooth ScrollSticky Cards Scroll

On This Page

Common mistakesReduced motionClean up everythingPointer, touch and keyboardListen on the containerGate hover effectsFocus parityAnimate cheap propertiesFrame-rate independent dampingCanvas and WebGLPreloadersMenusRoute changesTextAssetsBudgets

Contribute

  • Report an issue
  • Request a feature
  • Edit this page