FootersSimpleApril 7, 2025

Footer Parallax Text

Large background text in footer drifts vertically as you scroll past it, creating a layered depth effect behind the footer content. Source: itsjay.us footer section.

View Full Demo →
demo.jsx
import FooterParallaxText from "./index.jsx";
import styles from "./demo.module.css";

const PROJECTS = [
  { name: "Sheabinta", cat: "Shopify" },
  { name: "PATHS Nonprofit", cat: "Web Design" },
  { name: "Socialstats", cat: "Branding" },
  { name: "Kevin Davis", cat: "Portfolio" },
  { name: "Delivrd", cat: "Web App" },
];

export default function FooterParallaxTextDemo() {
  return (
    <div>
      {/* Hero — dark full-viewport */}
      <section className={styles.hero}>
        <h1 className={styles.heroTitle}>
          <span>Motion-forward</span>
          <span>design studio</span>
        </h1>
        <p className={styles.heroSub}>No-5 Studio · New York</p>
      </section>

      {/* Work section — light, gives vertical space above the footer */}
      <section className={styles.work}>
        <p className={styles.workLabel}>Selected work — 2023/25</p>
        <ul className={styles.workList}>
          {PROJECTS.map((p) => (
            <li key={p.name} className={styles.workItem}>
              <span className={styles.workName}>{p.name}</span>
              <span className={styles.workCat}>{p.cat}</span>
            </li>
          ))}
        </ul>
      </section>

      {/* The actual component — scroll here to see the parallax text */}
      <FooterParallaxText
        backgroundText="no-5.studio"
        navLinks={[
          { label: "Work", href: "#" },
          { label: "About", href: "#" },
          { label: "Contact", href: "#" },
        ]}
        email="hello@no-5.studio"
        copyright="© 2025 No-5 Studio. All rights reserved."
      />
    </div>
  );
}
index.jsx
"use client";

import { useRef } from "react";
import { useScroll, useTransform, motion } from "framer-motion";
import styles from "./styles.module.css";

export default function FooterParallaxText({
  backgroundText = "no-5.studio",
  navLinks = [],
  email = "",
  copyright = "",
}) {
  const ref = useRef(null);

  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"],
  });

  const y = useTransform(scrollYProgress, [0, 1], ["-10vh", "10vh"]);

  return (
    <footer ref={ref} className={styles.footer}>
      <motion.h2
        className={styles.backgroundText}
        style={{ y }}
        aria-hidden="true"
      >
        {backgroundText}
      </motion.h2>

      <div className={styles.content}>
        <div className={styles.top}>
          <p className={styles.email}>{email}</p>
          <nav className={styles.nav}>
            {navLinks.map((link) => (
              <a key={link.label} href={link.href} className={styles.navLink}>
                {link.label}
              </a>
            ))}
          </nav>
        </div>

        <div className={styles.bottom}>
          <p className={styles.copyright}>{copyright}</p>
        </div>
      </div>
    </footer>
  );
}
demo.module.css
.hero {
  height: 100vh;
  background: #0d0d0d;
  color: #ffffff;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 3rem;
}

.heroTitle {
  display: flex;
  flex-direction: column;
  font-size: clamp(3rem, 8vw, 7.5rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 0.95;
  margin-bottom: 2rem;
}

.heroSub {
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.35;
}

.work {
  background: #f5f5f0;
  padding: 3rem 3rem 4rem;
}

.workLabel {
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #9b9b9b;
  margin-bottom: 2rem;
}

.workList {
  list-style: none;
  padding: 0;
  margin: 0;
}

.workItem {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.5rem 0;
  border-top: 1px solid #e5e5e5;
}

.workItem:last-child {
  border-bottom: 1px solid #e5e5e5;
}

.workName {
  font-size: clamp(1.25rem, 2.5vw, 2rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: #1a1a1a;
}

.workCat {
  font-size: 0.8125rem;
  color: #9b9b9b;
}
styles.module.css
.footer {
  position: relative;
  overflow: hidden;
  background-color: #111111;
  color: #ffffff;
  padding: 6rem 3rem 3rem;
  min-height: 280px;
}

.backgroundText {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(4rem, 12vw, 10rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  color: #ffffff;
  opacity: 0.06;
  white-space: nowrap;
  user-select: none;
  will-change: transform;
  pointer-events: none;
  z-index: 0;
}

.content {
  position: relative;
  z-index: 1;
  max-width: 1100px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 3rem;
}

.top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.email {
  font-size: clamp(1rem, 2vw, 1.25rem);
  color: rgba(255, 255, 255, 0.6);
}

.nav {
  display: flex;
  gap: 2rem;
}

.navLink {
  font-size: 0.9375rem;
  color: rgba(255, 255, 255, 0.7);
  text-decoration: none;
  transition: color 250ms ease;
}

.navLink:hover {
  color: #ffffff;
}

.bottom {
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding-top: 1.5rem;
}

.copyright {
  font-size: 0.8125rem;
  color: rgba(255, 255, 255, 0.4);
}
  • framer-motion