Giant Wordmark Footer
Footer with a giant wordmark: dark agency footer with sitemap and contacts, closed by a wordmark that rises letter by letter.
- Footers
- Creative agency
- React only · 0 deps
- 1 file
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/giant-wordmark-footer.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/footers/giant-wordmark-footer.tsx
1/**2 * GiantWordmarkFooter, a dark agency footer: a new-business pitch with a big3 * email, three sitemap columns and a studio address, closed by a wordmark that4 * spans the full width exactly. The wordmark is fitted by measuring the text5 * at a reference size and scaling (re-fitted on resize), then each letter6 * rises out of a mask, staggered, when the footer scrolls into view. Letters7 * nudge up and take the signal colour on hover.8 *9 * Props: wordmark, pitch, contacts, columns, address, legal, tagline.10 * CSS vars on the root: --fd-gwf-bg, --fd-gwf-fg, --fd-gwf-signal.11 *12 * Browser support: all evergreen browsers (ResizeObserver, IntersectionObserver).13 * Accessibility: the wordmark is one heading with an aria-label; the per-letter14 * spans are aria-hidden. Links have visible focus rings. Reduced motion shows15 * the wordmark immediately without the rise or the hover nudge.16 * SSR: renders with a vw-based fallback size; fitting happens in an effect.17 */18import { useEffect, useLayoutEffect, useRef, useState } from "react";19 20export interface GwfLink { label: string; href: string; note?: string }21export interface GwfColumn { title: string; links: GwfLink[] }22export interface GwfContact { label: string; name: string; email: string; phone?: string }23 24export interface GiantWordmarkFooterProps {25 wordmark?: string;26 pitch?: [string, string];27 contacts?: GwfContact[];28 columns?: GwfColumn[];29 address?: string[];30 legal?: string[];31 tagline?: string;32 className?: string;33}34 35const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;36 37const DEFAULT_COLUMNS: GwfColumn[] = [38 {39 title: "Work",40 links: [41 { label: "Brightwater Ferries", href: "#", note: "Identity" },42 { label: "Common Loaf", href: "#", note: "Packaging" },43 { label: "Tidal Records", href: "#", note: "Motion" },44 { label: "Northbank Opera", href: "#", note: "Season" },45 { label: "All projects", href: "#", note: "48" },46 ],47 },48 {49 title: "Studio",50 links: [51 { label: "About", href: "#" },52 { label: "People", href: "#", note: "19" },53 { label: "Jobs", href: "#", note: "2 open" },54 { label: "Notes", href: "#" },55 ],56 },57 {58 title: "Elsewhere",59 links: [60 { label: "Instagram", href: "#" },61 { label: "Are.na", href: "#" },62 { label: "Vimeo", href: "#" },63 { label: "LinkedIn", href: "#" },64 ],65 },66];67 68const DEFAULT_CONTACTS: GwfContact[] = [69 { label: "New work", name: "Priya Raman", email: "priya@oddweather.studio", phone: "+44 20 7946 0321" },70 { label: "Press, talks, students", name: "Tom Achterberg", email: "hello@oddweather.studio" },71];72 73export function GiantWordmarkFooter({74 wordmark = "Odd Weather",75 pitch = ["Got a strange brief?", "Good. Send it over."],76 contacts = DEFAULT_CONTACTS,77 columns = DEFAULT_COLUMNS,78 address = ["Unit 7, 41 Hackney Road", "London E2 7NX"],79 legal = ["© 2026 Odd Weather Ltd.", "Company no. 11290473", "Privacy"],80 tagline = "Mostly sunny. Occasionally odd.",81 className,82}: GiantWordmarkFooterProps) {83 const boxRef = useRef<HTMLDivElement>(null);84 const probeRef = useRef<HTMLSpanElement>(null);85 const markRef = useRef<HTMLHeadingElement>(null);86 const [size, setSize] = useState<number | null>(null);87 const [shown, setShown] = useState(false);88 const letters = Array.from(wordmark);89 90 useIso(() => {91 const box = boxRef.current, probe = probeRef.current;92 if (!box || !probe) return;93 const fit = () => {94 const w = probe.getBoundingClientRect().width; // measured at 100px95 const avail = box.clientWidth;96 if (w > 0 && avail > 0) setSize(Math.floor((avail / w) * 100 * 100) / 100);97 };98 fit();99 const ro = new ResizeObserver(fit);100 ro.observe(box);101 return () => ro.disconnect();102 }, [wordmark]);103 104 useEffect(() => {105 const el = markRef.current;106 if (!el) return;107 if (matchMedia("(prefers-reduced-motion: reduce)").matches) { setShown(true); return; }108 const io = new IntersectionObserver((es) => {109 if (es.some((e) => e.isIntersecting)) { setShown(true); io.disconnect(); }110 }, { threshold: 0.35 });111 io.observe(el);112 return () => io.disconnect();113 }, []);114 115 return (116 <footer className={`fd-gwf${className ? " " + className : ""}`}>117 <style>{css}</style>118 <div className="fd-gwf-in">119 <div className="fd-gwf-top">120 <div className="fd-gwf-pitch">121 <p className="fd-gwf-kicker"><span aria-hidden="true">●</span> Taking on two projects for spring 2027</p>122 <h2 className="fd-gwf-h">123 <span>{pitch[0]}</span>124 <span className="fd-gwf-h2">{pitch[1]}</span>125 </h2>126 {contacts[0] && (127 <a className="fd-gwf-big" href={`mailto:${contacts[0].email}`}>128 <span>{contacts[0].email}</span>129 <svg width="28" height="28" viewBox="0 0 28 28" fill="none" stroke="currentColor" strokeWidth="2.2" aria-hidden="true"><path d="M7 21L21 7M10 7h11v11" /></svg>130 </a>131 )}132 </div>133 <nav className="fd-gwf-cols" aria-label="Footer">134 {columns.map((c) => (135 <div className="fd-gwf-col" key={c.title}>136 <h3 className="fd-gwf-lab">{c.title}</h3>137 <ul className="fd-gwf-list">138 {c.links.map((l) => (139 <li key={l.label}>140 <a className="fd-gwf-a" href={l.href}><span>{l.label}</span></a>141 {l.note && <span className="fd-gwf-note">{l.note}</span>}142 </li>143 ))}144 </ul>145 </div>146 ))}147 </nav>148 </div>149 150 <div className="fd-gwf-mid">151 {contacts.map((c, i) => (152 <div className="fd-gwf-cell" key={c.email}>153 <p className="fd-gwf-lab">{c.label}</p>154 <p className="fd-gwf-txt">{c.name}</p>155 {i > 0 && <a className="fd-gwf-a fd-gwf-txt" href={`mailto:${c.email}`}><span>{c.email}</span></a>}156 {c.phone && <a className="fd-gwf-a fd-gwf-txt" href={`tel:${c.phone.replace(/\s/g, "")}`}><span>{c.phone}</span></a>}157 </div>158 ))}159 <div className="fd-gwf-cell">160 <p className="fd-gwf-lab">Studio</p>161 {address.map((l) => <p className="fd-gwf-txt" key={l}>{l}</p>)}162 <a className="fd-gwf-a fd-gwf-txt" href="#"><span>Directions</span></a>163 </div>164 </div>165 166 <div className="fd-gwf-box" ref={boxRef}>167 <span className="fd-gwf-probe" ref={probeRef} aria-hidden="true">168 {letters.map((ch, i) => <span key={i} data-last={i === letters.length - 1 ? "" : undefined}>{ch === " " ? "\u00A0" : ch}</span>)}169 </span>170 <h2171 ref={markRef}172 className="fd-gwf-mark"173 data-in={shown ? "" : undefined}174 aria-label={wordmark}175 style={size ? { fontSize: `${size}px` } : undefined}176 >177 {letters.map((ch, i) => (178 <span className="fd-gwf-cl" key={i} aria-hidden="true">179 <span180 className="fd-gwf-ch"181 data-last={i === letters.length - 1 ? "" : undefined}182 style={{ transitionDelay: shown ? `${i * 48}ms` : "0ms" }}183 >{ch === " " ? " " : ch}</span>184 </span>185 ))}186 </h2>187 </div>188 189 <div className="fd-gwf-legal">190 <ul className="fd-gwf-ll">191 {legal.map((l) => <li key={l}>{l === "Privacy" ? <a className="fd-gwf-a" href="#"><span>{l}</span></a> : l}</li>)}192 </ul>193 <p>{tagline}</p>194 </div>195 </div>196 </footer>197 );198}199 200const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;201const MONO = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;202 203const css = `204:where(.fd-gwf,.fd-gwf *){box-sizing:border-box;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}205:where(.fd-gwf,.fd-gwf *)::before,:where(.fd-gwf,.fd-gwf *)::after{box-sizing:border-box}206:where(.fd-gwf svg){display:block;flex-shrink:0}207.fd-gwf{208 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;--fd-dur-4:700ms;209 --fd-gwf-bg:#0F0F0D;--fd-gwf-fg:#ECEAE3;--fd-gwf-mut:#8C8A82;--fd-gwf-line:rgba(236,234,227,.14);--fd-gwf-signal:#D7FF3B;210 display:block;width:100%;background:var(--fd-gwf-bg);color:var(--fd-gwf-fg);211 font:400 15px/22px ${SANS};letter-spacing:-.005em;-webkit-font-smoothing:antialiased;overflow:hidden}212.fd-gwf-in{padding:clamp(56px,7vw,96px) clamp(16px,3.4vw,48px) 0}213.fd-gwf-top{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:48px clamp(32px,6vw,96px);padding-bottom:56px}214.fd-gwf-kicker{display:flex;align-items:center;gap:10px;font:500 12px/16px ${MONO};letter-spacing:.02em;color:var(--fd-gwf-mut)}215.fd-gwf-kicker span{color:var(--fd-gwf-signal);font-size:9px;line-height:16px;animation:fd-gwf-blink 2.4s steps(1) infinite}216@keyframes fd-gwf-blink{50%{opacity:.25}}217.fd-gwf-h{margin-top:20px;font:800 clamp(2.25rem,4.6vw,4rem)/.98 ${SANS};letter-spacing:-.045em}218.fd-gwf-h span{display:block}219.fd-gwf-h2{color:var(--fd-gwf-mut)}220.fd-gwf-big{display:inline-flex;align-items:center;gap:14px;margin-top:32px;padding-bottom:6px;font:600 clamp(1.25rem,2.2vw,1.75rem)/1.2 ${SANS};letter-spacing:-.02em;color:var(--fd-gwf-fg);box-shadow:inset 0 -2px 0 var(--fd-gwf-signal);transition:color var(--fd-dur-2) var(--fd-ease-out);max-width:100%}221.fd-gwf-big span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}222.fd-gwf-big svg{transition:transform var(--fd-dur-3) var(--fd-spring)}223.fd-gwf-big:hover{color:var(--fd-gwf-signal)}224.fd-gwf-big:hover svg{transform:translate(4px,-4px) rotate(45deg)}225.fd-gwf-cols{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:32px;align-content:start}226.fd-gwf-lab{font:500 11px/16px ${MONO};letter-spacing:.08em;text-transform:uppercase;color:var(--fd-gwf-mut)}227.fd-gwf-list{margin-top:16px;display:grid;gap:8px}228.fd-gwf-list li{display:flex;align-items:baseline;gap:8px;min-width:0}229.fd-gwf-note{font:400 11px/16px ${MONO};color:var(--fd-gwf-mut);white-space:nowrap}230.fd-gwf-a{position:relative;display:inline-block;color:var(--fd-gwf-fg);border-radius:2px;outline:0}231.fd-gwf-a span{background:linear-gradient(var(--fd-gwf-signal),var(--fd-gwf-signal)) 0 100%/0 1px no-repeat;transition:background-size var(--fd-dur-3) var(--fd-ease-out),color var(--fd-dur-1)}232.fd-gwf-a:hover span{background-size:100% 1px;color:var(--fd-gwf-signal)}233.fd-gwf-a:focus-visible,.fd-gwf-big:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-gwf-signal) 70%,transparent);outline-offset:2px}234.fd-gwf-mid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;padding:28px 0 32px;border-top:1px solid var(--fd-gwf-line)}235.fd-gwf-cell{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0}236.fd-gwf-cell .fd-gwf-lab{margin-bottom:10px}237.fd-gwf-txt{font:400 15px/22px ${SANS};color:var(--fd-gwf-fg);max-width:100%;overflow-wrap:anywhere}238.fd-gwf-box{position:relative;width:100%;padding:8px 0 clamp(8px,1.2vw,18px)}239.fd-gwf-probe{position:absolute;left:0;top:0;display:inline-flex;visibility:hidden;white-space:pre;pointer-events:none;font:800 100px/1 ${SANS};letter-spacing:-.06em}240.fd-gwf-probe span{display:block}.fd-gwf-probe span[data-last]{letter-spacing:0}241.fd-gwf-mark{display:flex;white-space:pre;font:800 18.6vw/.82 ${SANS};letter-spacing:-.06em;color:var(--fd-gwf-fg);user-select:none}242.fd-gwf-cl{display:block;overflow:hidden;padding:.14em 0 .02em;margin:-.14em 0 -.02em}243.fd-gwf-ch{display:block;transform:translateY(102%) rotate(6deg);transform-origin:0 100%;transition:transform var(--fd-dur-4) var(--fd-ease-out),color var(--fd-dur-2) var(--fd-ease-out)}244.fd-gwf-ch[data-last]{letter-spacing:0}245.fd-gwf-mark[data-in] .fd-gwf-ch{transform:none}246@media (hover:hover){247 .fd-gwf-mark[data-in] .fd-gwf-cl:hover .fd-gwf-ch{transform:translateY(-5%);color:var(--fd-gwf-signal);transition:transform var(--fd-dur-3) var(--fd-spring),color var(--fd-dur-1);transition-delay:0ms!important}248}249.fd-gwf-legal{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;padding:20px 0 24px;border-top:1px solid var(--fd-gwf-line);font:400 12px/16px ${MONO};color:var(--fd-gwf-mut)}250.fd-gwf-ll{display:flex;flex-wrap:wrap;gap:6px 24px}251.fd-gwf-legal .fd-gwf-a{color:var(--fd-gwf-mut)}252@media (max-width:900px){253 .fd-gwf-top{grid-template-columns:1fr}254}255@media (max-width:640px){256 .fd-gwf-cols{grid-template-columns:1fr 1fr}257 .fd-gwf-cols .fd-gwf-col:first-child{grid-column:1/-1}258 .fd-gwf-mid{grid-template-columns:1fr;gap:28px}259 .fd-gwf-legal{flex-direction:column;align-items:flex-start}260}261@media (prefers-reduced-motion:reduce){262 .fd-gwf-ch{transform:none!important;transition:color var(--fd-dur-1)!important}263 .fd-gwf-kicker span{animation:none}264 .fd-gwf *{transition-duration:1ms}265}266@media (prefers-contrast:more){.fd-gwf{--fd-gwf-mut:#C9C7BF;--fd-gwf-line:rgba(236,234,227,.5)}}267`;268 269/* ───────────────────────────── demo ───────────────────────────── */270 271export function Demo() {272 return (273 <div className="fd-gwfd">274 <style>{demoCss}</style>275 <section className="fd-gwfd-end" aria-label="End of case study">276 <div className="fd-gwfd-credits">277 <p className="fd-gwfd-k">Credits</p>278 <p>Creative direction, Priya Raman. Type and motion, Jonas Ek, Ama Owusu. Photography, Leon Brandt. Built with the ferry crews of Brightwater, 2025 to 2026.</p>279 </div>280 <a className="fd-gwfd-next" href="#">281 <span className="fd-gwfd-k">Next case, 06 of 48</span>282 <span className="fd-gwfd-t">Tidal Records</span>283 <span className="fd-gwfd-s">A label that sounds like the sea, and now looks like it too.</span>284 <svg width="40" height="40" viewBox="0 0 40 40" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M8 20h24M22 10l10 10-10 10" /></svg>285 </a>286 </section>287 <GiantWordmarkFooter />288 </div>289 );290}291 292const demoCss = `293:where(.fd-gwfd,.fd-gwfd *){box-sizing:border-box;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none}294:where(.fd-gwfd svg){display:block}295.fd-gwfd{min-height:100vh;background:#0F0F0D;color:#ECEAE3;font:400 15px/22px ${SANS};-webkit-font-smoothing:antialiased}296.fd-gwfd-end{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr);gap:32px;padding:56px clamp(16px,3.4vw,48px) 0}297.fd-gwfd-k{display:block;font:500 11px/16px ${MONO};letter-spacing:.08em;text-transform:uppercase;color:#8C8A82}298.fd-gwfd-credits p:last-child{margin-top:12px;max-width:380px;color:#B9B7AF;font-size:14px;line-height:21px}299.fd-gwfd-next{position:relative;display:block;padding:24px 72px 28px 0;border-top:1px solid rgba(236,234,227,.14);border-bottom:1px solid rgba(236,234,227,.14);border-radius:2px;outline:0}300.fd-gwfd-next:focus-visible{outline:2px solid rgba(215,255,59,.7);outline-offset:2px}301.fd-gwfd-t{display:block;margin-top:8px;font:800 clamp(2rem,4vw,3.5rem)/1 ${SANS};letter-spacing:-.04em;transition:color 240ms cubic-bezier(.22,1,.36,1)}302.fd-gwfd-s{display:block;margin-top:10px;color:#8C8A82}303.fd-gwfd-next svg{position:absolute;right:0;top:50%;margin-top:-20px;transition:transform 420ms cubic-bezier(.34,1.56,.64,1)}304.fd-gwfd-next:hover .fd-gwfd-t{color:#D7FF3B}305.fd-gwfd-next:hover svg{transform:translateX(8px)}306@media (max-width:760px){.fd-gwfd-end{grid-template-columns:1fr}}307`;More Footers. Same file contract.
Questions about Giant Wordmark Footer
Run npx shadcn@latest add https://freedesignmd.com/r/giant-wordmark-footer.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
Footer with a giant wordmark: dark agency footer with sitemap and contacts, closed by a wordmark that rises letter by letter. It is a free, MIT-licensed React footers component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers (ResizeObserver, IntersectionObserver).
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


