Skip to content

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
Full screen

Browser support · All evergreen browsers (ResizeObserver, IntersectionObserver).

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 big
3 * email, three sitemap columns and a studio address, closed by a wordmark that
4 * spans the full width exactly. The wordmark is fitted by measuring the text
5 * at a reference size and scaling (re-fitted on resize), then each letter
6 * rises out of a mask, staggered, when the footer scrolls into view. Letters
7 * 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-letter
14 * spans are aria-hidden. Links have visible focus rings. Reduced motion shows
15 * 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 100px
95 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 <h2
171 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 <span
180 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.

Curtain Reveal Footer, footers preview: Curtain reveal footer fixed beneath the page and uncovered as content lifts away, with tables, hours, address…

Curtain Reveal Footer

Curtain reveal footer fixed beneath the page and uncovered as content lifts away, with tables, hours, address and a Book call.

Office Clocks Footer, footers preview: Footer with live office clocks: analog dials show local time and open or closed state, plus a UTC ruler and…

Office Clocks Footer

Footer with live office clocks: analog dials show local time and open or closed state, plus a UTC ruler and newsletter.

Developer Footer with Status, footers preview: Developer footer with a live status pill and per-region uptime bars, sitemap, validated newsletter and…

Developer Footer with Status

Developer footer with a live status pill and per-region uptime bars, sitemap, validated newsletter and language select.

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.