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. Motion Principles

Motion Principles

PreviousNext

The easing, timing and choreography rules behind every Tween component.

Tween components share one motion vocabulary. When you build your own sections next to them, use the same curves, durations and sequencing rules so the page reads as one piece of work instead of a collage of demos.

Rule of thumb: one set of eases, one timing scale, one timeline per scene. Everything below is a way of keeping to those three.

Easing

A signature curve

Pick one strong in-out curve and use it for every large move: preloaders, page transitions, menus and panels. A steep start and a long, soft landing reads as deliberate and calm.

import { gsap } from "gsap"
import { CustomEase } from "gsap/CustomEase"
 
if (typeof window !== "undefined") {
  gsap.registerPlugin(CustomEase)
}
 
// Register once, at module level, with a name that is unlikely to collide.
CustomEase.create("tween-hop", "0.9, 0, 0.1, 1")
CustomEase.create("tween-glide", "0.8, 0, 0.2, 1")

The CSS and Web Animations twin of tween-hop is cubic-bezier(0.87, 0, 0.13, 1). Use it for hover fills, clip-path transitions and anything you write in plain CSS so both layers move the same way.

Give custom eases unique names. Many snippets on the web register a curve called hop, and two different hop curves on the same page silently overwrite each other.

Eases by job

JobEaseTypical duration
Text lines rising from a maskpower3.out or power4.out0.75–1 s
Panels, menus, overlayspower3.inOut or power4.inOut1–1.25 s
Long decelerationsexpo.out1–1.5 s
Fades, progress barspower2.inOut0.3–0.6 s
Big wipes, shared elementsyour signature curve1–1.75 s
Scrubbed scroll, marquees"none"set by scroll

Keep elastic and back.out for physics toys. On editorial layouts they look like a bounce bug.

Shaping scrubbed motion

A scrubbed tween should use ease: "none" so it tracks the scrollbar exactly. When a scrubbed value needs to feel softer, shape the progress yourself instead of easing the tween:

const smoothstep = (p: number) => p * p * (3 - 2 * p)
 
ScrollTrigger.create({
  trigger: section,
  scrub: true,
  onUpdate: (self) => {
    gsap.set(image, { scale: 1.2 - 0.2 * smoothstep(self.progress) })
  },
})

Or put the eased tween inside a scrubbed timeline: the timeline stays linear, the child keeps its own curve.

Timing

ElementDurationStagger
Text lines1 s0.1 s
Words0.75–1 s0.05–0.075 s
Characters0.5–1 s0.015–0.05 s
Random "flicker" reveal0.05 s per step{ amount: 0.5, from: "random" }
UI entrances0.6–1 s0.05–0.1 s
Hover micro-feedback0.25 s—
Hover state change0.5 s—
Hover expansion0.75 s—
Hero zooms1.5–2.5 s—
Page transitions≤ 1.2 s felt in total—
Preloader to readable hero≤ 2.5 s—

Two numbers matter more than the rest:

  • Page transitions should feel finished in about a second. Anything longer starts to feel like waiting.
  • Preloaders should reach a readable hero within 2.5 seconds, and show a much shorter version (or nothing) on later visits in the same session.

Choreography

One scene, one job

Treat every section as a 100svh scene that does one thing: a statement, a reveal, a stack, a gallery. Bookend each pinned section with a calm intro and outro so the user always has a resting state before the next effect starts.

One timeline per scene

Write a scene as a single timeline with labels rather than a pile of independent tweens with delays. Labels make retiming a one-line change:

const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 1 } })
 
tl.addLabel("frame")
  .to(frame, { clipPath: "inset(0% 0% 0% 0%)", ease: "tween-hop" }, "frame")
  .addLabel("copy", "frame+=0.6")
  .to(lines, { yPercent: 0, stagger: 0.1 }, "copy")
  .to(cta, { autoAlpha: 1, duration: 0.6 }, "copy+=0.3")

Hand off with promises, not delays

When one sequence has to wait for another (a preloader handing off to a hero, a transition cover handing off to a route change), expose the moment as a promise or a timeline callback. Do not guess with a hard-coded delay; the next person to change the preloader length will break the hero.

function playIntro(): Promise<void> {
  return new Promise((resolve) => {
    gsap
      .timeline({ onComplete: resolve })
      .to(overlay, { clipPath: "inset(0% 0% 100% 0%)", ease: "tween-hop" })
  })
}
 
await playIntro()
revealHero()

The same rule applies to page transitions: navigate only when the cover is fully closed, from the cover tween's onComplete, then reset scroll and call ScrollTrigger.refresh() before uncovering.

Interaction comes last

Enable hover and pointer effects only after a section's intro has finished. A hover that fires halfway through an entrance fights the timeline and leaves elements in mixed states.

Text with SplitText

Most Tween headings use one pattern: split into lines, mask each line, and slide the lines up from below. See Text Reveal Scroll and Block Text Reveal for complete components.

"use client"
 
import * as React from "react"
import { gsap } from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { SplitText } from "gsap/SplitText"
 
import { usePreviewScroller } from "@/registry/lib/preview-scroller"
 
if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger, SplitText)
}
 
export function LineReveal({
  children,
  scroller,
}: {
  children: React.ReactNode
  scroller?: HTMLElement | string | null
}) {
  const ref = React.useRef<HTMLHeadingElement>(null)
  const ambientScroller = usePreviewScroller()
  const activeScroller = scroller ?? ambientScroller
 
  React.useEffect(() => {
    const el = ref.current
    if (!el) return
 
    const reduceMotion =
      typeof window !== "undefined" &&
      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
    if (reduceMotion) return // the heading is already in its final state
 
    let split: SplitText | null = null
    let cancelled = false
 
    // Split after fonts load so line breaks match the final layout.
    document.fonts.ready.then(() => {
      if (cancelled) return
      split = SplitText.create(el, {
        type: "lines",
        mask: "lines",
        autoSplit: true,
        onSplit(self) {
          // Returning the tween lets SplitText kill it on re-split.
          return gsap.from(self.lines, {
            yPercent: 100,
            duration: 1,
            stagger: 0.1,
            ease: "power4.out",
            scrollTrigger: {
              scroller: activeScroller ?? undefined,
              trigger: el,
              start: "top 80%",
              once: true,
            },
          })
        },
      })
    })
 
    return () => {
      cancelled = true
      split?.revert()
    }
  }, [activeScroller])
 
  return <h2 ref={ref}>{children}</h2>
}

Notes on the pattern:

  • Return the tween from onSplit. With autoSplit, SplitText re-splits on resize. Returning the animation lets it kill the old one; otherwise triggers stack up on every resize.
  • Mask level matches the role. Use mask: "lines" or "words" for headlines and body copy, mask: "chars" for short brand words, and exit through the same mask with yPercent: -100.
  • Text indent. SplitText wraps lines in blocks, so a CSS text-indent lands on every line. Move it to the first line as padding-left and set the element's indent to 0, the way TextRevealScroll does.
  • 3D variant. Split into "words,chars", rotate characters from rotationX: -90 with transformOrigin: "50% 50% -50px", and give the parent perspective: 700px.
  • Always revert() on unmount. It restores the original text node for screen readers and for React.

Design language

Motion lands best on a layout that is built for it.

  • Layout. Full-height scenes, a centred statement at 45–60% width, or a full-bleed image with a bottom-anchored headline. Statements read best at a narrow measure of about 14–35 characters per line. Use flex ratios (2:1, 4:3) more than rigid column counts, and design the narrow phone width first.
  • Corner chrome. Small uppercase mono labels fixed in the corners (a section index, a status word, a local clock) give a page structure without competing with the content.
  • Type. Pair one display face, one neutral body face and one uppercase mono for labels, links and buttons. Work at two extremes of scale: display type at clamp(4rem, 12vw, 18rem) against 10–16 px labels. Tight leading (0.8–0.95) and slightly negative tracking suit display type; medium-weight sentence case reads calmer than heavy all-caps.
  • Colour. A near-black ground rather than pure black (around #0f0f0f), a warm off-white second surface, and a single accent. Keep the UI monochrome and let the imagery carry the colour.
  • Imagery. Grade every image in a set to one palette. Common ratios: 4:5 or 2:3 for people, 1:1 for products, 16:9 or 3:2 for full-bleed.
  • Radius ladder. Square corners for engineered looks, 0.25–0.75 rem for editorial work, 1–2 rem for friendly panels, full pills only for buttons and tags.
  • Text over images. White text with a 25–50% black veil, or mix-blend-mode: difference for fixed navigation over mixed backgrounds.

Next: Scroll and Smooth Scroll covers how these timelines connect to the scrollbar.

StoryScroll and Smooth Scroll

On This Page

EasingA signature curveEases by jobShaping scrubbed motionTimingChoreographyOne scene, one jobOne timeline per sceneHand off with promises, not delaysInteraction comes lastText with SplitTextDesign language

Contribute

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