18 — Bringing It to Life

✨ Chapter 18 — Bringing It to Life

Every chapter so far has been pure CSS. This one adds a small, deliberate amount of real JavaScript — just enough to make two effects work that CSS genuinely cannot do alone: detecting when an element scrolls into view, and reacting to the page's scroll position. Everything else about how these effects look is still CSS; JavaScript's only job is to flip a class on and off at the right moment.

13
JavaScript Helpers — scroll reveal + nav shadow

Part 1 — The .reveal CSS

Before writing any JavaScript, we define the two CSS states the observer will switch between. Elements start hidden and translated down; the observer adds .visible to animate them into place.

🎨 style.css — .reveal states
/* ── Scroll reveal ───────────────────────────────── */ .reveal { opacity: 0; transform: translateY(24px); /* starts 24 px below final position */ transition: opacity 0.6s ease, transform 0.6s ease; } .reveal.visible { opacity: 1; transform: translateY(0); } /* Accessibility: skip animation for users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; /* immediately visible */ transform: none; transition: none; } } /* Nav shadow when scrolled */ .site-header.scrolled { box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
♿ prefers-reduced-motion: Some users enable this OS setting because motion causes discomfort, dizziness, or triggers vestibular disorders. The @media (prefers-reduced-motion: reduce) block makes all .reveal elements immediately visible — no fade, no slide. Always include this when adding scroll animations.

Part 2 — IntersectionObserver (the reveal)

The old approach to scroll animations was a scroll event listener that ran on every pixel of scroll and called getBoundingClientRect() — expensive and janky. IntersectionObserver is the modern replacement: it tells the browser "notify me when this element crosses the viewport threshold" and fires asynchronously on the main thread only when needed.

1
Create the observer
Pass a callback and options. threshold: 0.15 means "fire when 15% of the element is visible."
2
Observe every .reveal element
Loop through querySelectorAll('.reveal') and register each one.
3
Callback fires when element enters viewport
entry.isIntersecting is true — add the .visible class to trigger the CSS transition.
4
Stop watching (fire once)
Call unobserve(entry.target) — once revealed, the element stays revealed. No need to keep watching it.
⚡ main.js — IntersectionObserver scroll reveal
// ── Scroll reveal ────────────────────────────────── const revealObserver = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); revealObserver.unobserve(entry.target); // fire once only } }); }, { threshold: 0.15 } // trigger when 15% visible ); document.querySelectorAll('.reveal').forEach(el => { revealObserver.observe(el); });

The observer fires asynchronously — it doesn't block the main thread on every scroll event, unlike a window.addEventListener('scroll', …) approach.

Part 3 — Nav scroll shadow

The sticky nav gains a box-shadow once the user scrolls past 20 px. A scroll event listener toggles the .scrolled class; the CSS transition on box-shadow (already on .site-header) makes it fade in smoothly.

⚡ main.js — nav scroll shadow
// ── Nav scroll shadow ────────────────────────────── const header = document.querySelector('.site-header'); window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 20); }, { passive: true }); // passive: true → browser can scroll without waiting

classList.toggle(className, condition) is a one-liner: adds the class when condition is true, removes it when false. No if/else needed.

passive: true tells the browser this listener will never call preventDefault(), so it doesn't need to wait for the listener to finish before scrolling. This eliminates the small lag that scroll listeners can introduce on mobile.

Live demo 1 — scroll reveal

Cards start hidden. Scroll down inside the box to watch them fade and rise into view. The status dots below track which cards have been revealed. Click Reset to run it again.

Card 1 Card 2 Card 3 Card 4

↓ scroll down to reveal cards

🎨

Brand Identity

Logos, colour systems, and visual language that last.

💻

Web Design

Sites that perform as beautifully as they look.

🎬

Motion

Animation that adds meaning, not just movement.

📐

Strategy

Decisions grounded in research, not guesswork.

Live demo 2 — nav scroll shadow

Scroll inside the box — the sticky nav gains a shadow as soon as you leave the top.

scroll below ↓
Section content — scroll up to see the nav shadow appear and disappear.
More content below…
Keep scrolling…
And back up — the shadow removes itself cleanly.
.scrolled class: inactive

Step 13 — complete

  • .reveal — hidden state: opacity: 0 + translateY(24px)
  • .reveal.visible — animated into place ✓
  • prefers-reduced-motion — skips animation for users who need it ✓
  • IntersectionObserver — adds .visible at 15% threshold, fires once ✓
  • .site-header.scrolled — nav shadow toggled by scroll position ✓
  • { passive: true } — scroll listener doesn't block the browser ✓
Next — Chapter 19: The finished Lumen Studio page, a chapter-by-chapter recap, and five "Go Further" challenges to extend it beyond the course.