Skip to content

Add restrained scroll-reveal animation system#22

Open
tiagov8 wants to merge 1 commit into
mainfrom
feature/scroll-reveal-animations
Open

Add restrained scroll-reveal animation system#22
tiagov8 wants to merge 1 commit into
mainfrom
feature/scroll-reveal-animations

Conversation

@tiagov8
Copy link
Copy Markdown
Member

@tiagov8 tiagov8 commented May 26, 2026

Summary

Adds a single, restrained scroll-reveal animation system across the homepage. Senior-designer feel: one easing curve, one distance, one duration, one stagger interval, applied consistently. No bounces, no scales, no rotations, no playful flourishes.

The system

Property Value Reason
Movement translateY(16px) → 0 + opacity 0 → 1 Subtle, doesn't compete with content
Duration 500ms Reads as intentional, not slow
Easing var(--ease-out) (cubic-bezier(0.16, 1, 0.3, 1)) Already in the design tokens; soft tail
Stagger 60ms between siblings (80ms for the 4-step horizontal row) Reads as a "wave" without dragging
Trigger IntersectionObserver, threshold: 0.15, rootMargin: '0px 0px -8% 0px' Fires ~85% into the viewport so it's confident, not premature
Persistence observer.unobserve() after first reveal Each element animates once; no re-firing on scroll-out/in

Implementation

CSS — one ruleset in src/styles/landing.css:

html.js .reveal {
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition: opacity 500ms var(--ease-out), transform 500ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
html.js .reveal.is-visible { opacity: 1; transform: none; will-change: auto; }
@media (prefers-reduced-motion: reduce) {
  html.js .reveal, html.js .reveal.is-visible {
    opacity: 1; transform: none; transition: none;
  }
}

JS bootstrap — a tiny inline <script is:inline> in <head> adds html.js before paint so no-FOUC; the existing end-of-body script adds the IntersectionObserver. Without JS or with prefers-reduced-motion: reduce users get the fully-visible static page.

Markup — applied class="reveal" + style={--reveal-delay: ${i * 60}ms} (per index) to every meaningful unit:

  • Hero copy — eyebrow → headline → lead → CTAs → checks (staggered 0/80/160/240/320ms — reads as one fluid hero entrance)
  • Stats — three stat cards (60ms stagger)
  • Section heads (.block-head / .faq-head) — kicker + title + lead reveal together as one unit
  • Card grids — benefits (×6), services (×4), how-it-works steps (×4 with 80ms stagger since they sit in a horizontal row), why-SSW rows (×5)
  • Testimonial aside — slight delay (120ms) so the why-list reveals first, then the quote
  • FAQ items — three accordion cards (60ms stagger)
  • Contact form + aside — form reveals first, aside trails by 120ms
  • CTA banner — title → description → button (80ms stagger)
  • Footer — brand reveals first, links column trails by 80ms

44 reveal elements total. None on the nav or the hero background image — those should be there from page paint.

What's deliberately NOT animated

Test plan

  • Open /, scroll slowly through each section — every grid reveals as a quiet wave; no juddering, no jumping past the fold
  • Hard refresh — hero copy reveals in cascade (eyebrow → headline → lead → CTAs → checks)
  • DevTools → emulate prefers-reduced-motion: reduce → all reveals are instant, full opacity
  • Disable JS → no FOUC, page renders fully visible immediately (the html.js class never gets added so the .reveal rules don't engage)
  • Open /admin/ and edit a benefit description → the live preview re-renders the section; reveal animation should fire once on the re-rendered card

🤖 Generated with Claude Code

Single coherent motion vocabulary across the page:
- One movement: translateY(16px) -> 0 + opacity 0 -> 1
- One duration: 500ms
- One easing: --ease-out (existing token, cubic-bezier(0.16, 1, 0.3, 1))
- One stagger interval: 60ms between siblings (80ms for the
  4-step horizontal Approach row)
- One trigger: IntersectionObserver, threshold 0.15, rootMargin
  '0px 0px -8% 0px', unobserve after first reveal

Architecture:
- CSS gated on html.js so no-JS users see content immediately
- Inline <script is:inline> in <head> sets html.js before paint
  (no FOUC)
- prefers-reduced-motion: reduce makes reveals instant
- IntersectionObserver also bails out under reduced-motion or
  when the API is unavailable

44 reveal elements applied: hero copy (staggered 0/80/160/240/320ms
for a single fluid hero entrance), stats, every .block-head, all
benefit/service/step/why cards, testimonial aside, FAQ items,
contact form + aside, CTA banner copy, footer brand + links column.

Deliberately untouched: nav (must be present from frame zero),
hero-bg photo (already has Ken Burns from PR #20), decorative
chrome, inline content links (own hover treatment from PR #17),
buttons (own micro-transitions).
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ssw-website-global 78a42ce Commit Preview URL

Branch Preview URL
May 26 2026, 11:39 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant