← Blurr Motion footer-css-scroll-driven-editorial
Categorie footers Tier 3 Techniek #40 Deps
— scroll om reveal te triggeren

Een footer die zich onthult terwijl je hem nadert. Pure CSS, native animation-timeline.

1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: footer-css-scroll-driven-editorial
// Pure CSS scroll-driven animations met animation-timeline: view().
// "Fin" letters reveal char-by-char via scroll-progress; tagline fade in op cover-range.
// JS-fallback: scroll-listener voor browsers zonder scroll-timeline support.
(() => {
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const supportsTimeline = CSS.supports('animation-timeline: view()');
  const root = document.querySelector('.fcs-ed');
  if (!root) return;
  const letters = root.querySelectorAll('.fcs-ed-fin span');
  const tagline = root.querySelector('.fcs-ed-tagline');
  if (reduce) {
    letters.forEach(l => { l.style.opacity = '1'; l.style.transform = 'translateY(0) rotate(0)'; });
    if (tagline) tagline.style.opacity = '1';
    return;
  }
  if (supportsTimeline) return; // CSS doet het werk
  const sentinel = root.querySelector('.fcs-ed-sentinel');
  if (!sentinel) return;
  const onScroll = () => {
    const r = sentinel.getBoundingClientRect();
    const vh = window.innerHeight;
    const p = Math.max(0, Math.min(1, 1 - (r.top / vh)));
    letters.forEach((l, i) => {
      const start = i * 0.08;
      const end = start + 0.45;
      const lp = Math.max(0, Math.min(1, (p - start) / (end - start)));
      l.style.opacity = String(lp);
      l.style.transform = 'translateY(' + ((1 - lp) * 60) + 'px) rotate(' + ((1 - lp) * -8) + 'deg)';
    });
    if (tagline) tagline.style.opacity = String(Math.max(0, Math.min(1, (p - 0.5) / 0.4)));
  };
  window.addEventListener('scroll', onScroll, { passive: true });
  onScroll();
})();
2. Skeleton — DOM + class-namen, mag herschikken
<!-- Skeleton: footer-css-scroll-driven-editorial -->
<div class="fcs-ed">
  <div class="fcs-ed-spacer"><span class="fcs-ed-eyebrow">scroll</span></div>
  <footer class="fcs-ed-foot">
    <div class="fcs-ed-sentinel"></div>
    <aside class="fcs-ed-side"><span class="fcs-ed-eyebrow">colophon</span></aside>
    <h2 class="fcs-ed-fin"><span>F</span><span>i</span><span>n</span><em>.</em></h2>
    <p class="fcs-ed-tagline">Crafted in Amsterdam.</p>
    <div class="fcs-ed-meta"><span>Issue 04</span><span>2026</span></div>
  </footer>
</div>
3. Styling-template — verplicht eigen invulling per merk
/* Styling: footer-css-scroll-driven-editorial */
:root {
  --ed-paper: #F4F1EB;
  --ed-ink: #0A0A0A;
  --ed-rule: rgba(10,10,10,.18);
  --ed-accent: #FF4A1C;
}
.fcs-ed { background: var(--ed-paper); color: var(--ed-ink); font-family: 'Fraunces', serif; }
.fcs-ed-foot { display: grid; grid-template-columns: minmax(140px,1fr) 4fr 1fr; }
.fcs-ed-fin span {
  display: inline-block; opacity: 0;
  transform: translateY(60px) rotate(-8deg);
  animation: edFin 1.4s cubic-bezier(.19,1,.22,1) both;
  animation-timeline: view();
  animation-range: entry 10% cover 55%;
}
.fcs-ed-tagline {
  opacity: 0;
  animation: edFade 1.6s ease-out both;
  animation-timeline: view();
  animation-range: cover 30% cover 80%;
}
@keyframes edFin { to { opacity: 1; transform: translateY(0) rotate(0); } }
@keyframes edFade { to { opacity: 1; } }
@supports not (animation-timeline: view()) { .fcs-ed-fin span, .fcs-ed-tagline { animation: none; } }
@media (prefers-reduced-motion: reduce) {
  .fcs-ed-fin span { opacity: 1 !important; transform: none !important; animation: none !important; }
  .fcs-ed-tagline { opacity: 1 !important; animation: none !important; }
}