---
title: "Effect downgrades: heavy library effect → cheap equivalent (inspire)"
description: "Keep the personality of an effect (timing, direction, restraint), rebuild it at the lowest tier that carries it. Tiers and budgets: motion §3."
canonical: https://void-design.vercel.app/docs/inspire/effect-downgrades
lastModified: 2026-09-16
---

# Effect downgrades: heavy library effect → cheap equivalent

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; `<video muted playsInline loop preload="none" 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 `<ViewTransition>` + CSS ≤ 400ms | T1 / 0 | recipes §11 |
| Shared-element morph (Framer `layoutId`, GSAP Flip) | +46 KB | `<ViewTransition name>` 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.
