Skip to content

Reference · inspire

Effect downgrades: heavy library effect → cheap equivalent

inspire/references/effect-downgrades.md110 linesupdated 16 Sept 2026

Keep the personality of an effect (timing, direction, restraint), rebuild it at the lowest tier that carries it. Tiers and budgets: motion §3. Recipes: motion/references/recipes.md (§ numbers below). Library-shaped effects: the components skill.

1. Mapping table

Source effect (typical stack) Cost in the source void equivalent Tier / JS Where
Preloader / intro video before content (hellohello ~8s) time-to-content, LCP delete; hero renders immediately, one ≤ 600ms lockup settle T1 / 0 recipes §10
Char-by-char hero reveal (GSAP SplitText, rAF) 45–70 KB, hidden LCP H1 void-settle (translate, no fade) + staggered sub/CTA void-rise T1 / 0 recipes §10
Masked line reveal on scroll (ScrollTrigger + translate3d(0,N%,0) in overflow-hidden) GSAP + ScrollTrigger ~70 KB, main-thread scroll authored lines + CSS view-timeline masked rise, one statement section T1 / 0 recipes §10
Per-word reveal (SplitText, Framer staggerChildren) per-node motion components server-split words + @starting-style / view() stagger T1 / 0 components → RevealText
Section fade-up on scroll (framer whileInView, AOS) +32–46 KB, JS per section reveal utility on 1–3 key visuals T1 / 0 craft / base.css
Scroll-scrubbed grey→white word fill ScrollTrigger scrub per-word color animation on a named view timeline (§2), one paragraph; or drop T1-paint / 0 below
Pinned full-viewport scenes (ScrollTrigger pin) scroll hijack feel, layout thrash one sticky track (h-[200svh] wrapper, sticky top-0 child) + CSS view-timeline for progress; or normal flow T1 / 0 below
Parallax image stacks (ScrollTrigger / Locomotive) main-thread transforms animation-timeline: view() translate ≤ 6% on one image per viewport T1 / 0 speed/references/rendering-smoothness.md §3
Smooth scroll (Lenis, Locomotive, ScrollSmoother) ~10 KB + permanent rAF, scroll on main thread native scroll; scroll-behavior: smooth for anchors only (base) motion §8
WebGL CRT / film / dither shader (OGL, three) 10–160 KB + GPU, context limits CSS scanlines + grain + inset vignette over a static AVIF; or one canvas under all rules T0 / 0 (or T4 budgeted) motion/references/webgl-canvas.md §7
Shader gradient / aurora / mesh (three, ogl, Stripe-like ribbon) 140 KB+ 2–3 blurred radial layers drifting via translate 20–40s, paused offscreen T1 / 0 components → AuroraBackdrop
Particle / star / flow field (tsParticles, canvas) perpetual rAF static SVG dot field + one slow layer translate; or useCanvas2D ≤ 30fps T0–T1 / 0, or T3 webgl-canvas.md §4
3D globe (three) 140 KB+ static SVG/AVIF; cobe (5 KB) only if it must rotate T0 / T4 components
Lottie illustrations lottie-web ~60 KB + JSON SVG with CSS keyframes on 1–3 paths, or an AVIF/short muted video with poster T1 / 0
Background video hero (Mux/HLS) 2–7 MB AVIF poster; started in view, pausable T0 / 0 speed
Custom cursor / cursor follower rAF + hidden native cursor drop; or spotlight gradient following pointer on one card grid, pointer-fine only T2 / ~0.5 KB components → SpotlightCard
Magnetic buttons window listeners + setState drop (hurts precision); or element-scoped translate ≤ 4px T2 / ~0.5 KB components → Magnetic
Infinite logo marquee (JS slider) perpetual JS loop, duplicate a11y CSS two-track marquee, inert duplicate, pause offscreen and on hover/focus T1 / 0 components → Marquee
Animated counters (CountUp, springs) SSR "0", replay on every view SSR the real value; animate once if it starts offscreen; or @number-flow/react T2 / ~0.6 KB components → NumberTicker
Border beam / animated gradient border infinite JS rotating conic layer with @property, one element T1 / 0 components → BeamBorder
Shiny / shimmer text perpetual JS CSS background-position keyframes on one short label, paused offscreen; never on headlines T1-paint / 0 components
Page transitions (Barba, GSAP Flip, framer AnimatePresence routes) JS orchestration React + CSS ≤ 400ms T1 / 0 recipes §11
Shared-element morph (Framer layoutId, GSAP Flip) +46 KB for route changes; LazyMotion layoutId only in-page when essential T1 / T2 recipes §11, motion §7
Hover-play project videos many videos preloading poster images; preload="none"; play on hover/focus under pointer-fine, one at a time T1 / small
Typewriter headline JS timers, CLS risk static headline; type a terminal log instead (fixed box, CSS steps()) T1 / 0 craft/references/directions.md terminal

2. Recipes not covered elsewhere

Scrubbed word fill (one paragraph; Chromium/Safari 26; static full color elsewhere):

tsx
const words = statement.split(" ");
<p className="void-fill text-display-sm">
  {words.map((w, i) => (
    <span key={i} className="void-fill-word" style={{ "--i": i / words.length } as React.CSSProperties}>{w} </span>
  ))}
</p>
css
@layer components {
  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
      .void-fill { view-timeline-name: --void-fill; }
      .void-fill-word {
        color: var(--fg-subtle);
        animation: void-fill linear both;
        animation-timeline: --void-fill;
        animation-range: cover calc(25% + var(--i) * 30%) cover calc(30% + var(--i) * 30%);
      }
    }
  }
}
@keyframes void-fill { to { color: var(--fg); } }

Color animation paints; keep it to one paragraph of ≤ 30 words. The text is in HTML either way.

Single sticky scene instead of a pinned timeline:

tsx
<section className="void-scene relative h-[200svh]" aria-labelledby="scene-title">
  <div className="sticky top-0 flex min-h-svh items-center">
    <Container className="grid gap-10 lg:grid-cols-12">
      <h2 id="scene-title" className="text-display text-fg lg:col-span-5">…</h2>
      <div className="void-scene-media lg:col-span-7">{/* real visual */}</div>
    </Container>
  </div>
</section>
css
@layer components {
  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) {
      .void-scene { view-timeline-name: --void-scene; }
      .void-scene-media { animation: void-scene linear both; animation-timeline: --void-scene; animation-range: contain 0% contain 100%; }
    }
  }
  @media (prefers-reduced-motion: reduce) { .void-scene { height: auto; } }
}
@keyframes void-scene { from { scale: 0.92; opacity: 0.4; } to { scale: 1; opacity: 1; } }

One per page. Firefox shows the final state in a tall sticky section; if that looks empty, drop h-[200svh] inside @supports not (animation-timeline: view()).

CRT look without a shader (terminal/agency hero):

tsx
<div className="relative overflow-clip rounded-xl bg-bg-subtle">
  <Image src={still} alt="…" className="h-auto w-full [filter:contrast(1.1)_saturate(0.8)]" />
  <div aria-hidden="true" className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(to_bottom,transparent_0_2px,color-mix(in_oklch,var(--bg)_55%,transparent)_2px_3px)] opacity-50" />
  <div aria-hidden="true" className="pointer-events-none absolute inset-0 shadow-[inset_0_0_120px_var(--bg)]" />
</div>

Add grain on the wrapper for noise. A slow roll bar (translate keyframes 7s linear infinite, data-fx/PauseOffscreen, none under reduced motion) is the only motion allowed here.

3. Deciding what to drop entirely

Drop when any is true: it's the source brand's most recognizable signature; it hides content or delays the fold; it hijacks scroll or the cursor; it needs > 1 WebGL context; it has no reduced-motion story and can't get one; it would be the second T2+ effect on the page. Replace with a stronger static idea (typography, a real product surface, a better photo) before adding any other motion.