Skip to content

Developer Footer with Status

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

  • Footers
  • Developer tools
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/dev-footer-status.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/footers/dev-footer-status.tsx
1/**
2 * DevFooterStatus, a dense, light footer for a developer platform. A brand
3 * column with a live status pill (click it for per-region 90-day uptime
4 * bars), a five-column sitemap, a changelog newsletter with real validation,
5 * loading and success states, a language select, hand-drawn SVG social icons
6 * and a mono legal row with the deployed build.
7 *
8 * Props: brand, blurb, columns, regions (code, name, uptime %, incidents as
9 * day indexes 0-89 → "degraded"), languages, socials, legal, build, onSubscribe
10 * (email) => Promise<void> | void.
11 * CSS vars: --fd-dfs-bg, --fd-dfs-fg, --fd-dfs-accent, --fd-dfs-ok.
12 *
13 * Browser support: all evergreen browsers.
14 * Accessibility: status pill is a button with aria-expanded controlling a
15 * labelled panel (Esc and outside click close it, focus returns). Uptime bars
16 * are summarised as text. The email field has a label, aria-invalid and an
17 * error tied with aria-describedby; success is announced politely. Social
18 * links have aria-labels. Reduced motion stops the status pulse.
19 */
20import { useEffect, useId, useRef, useState } from "react";
21import type { FormEvent, ReactNode } from "react";
22
23export interface DfsColumn { title: string; links: { label: string; href: string; badge?: string }[] }
24export interface DfsRegion { code: string; name: string; uptime: string; incidents?: number[]; minor?: number[] }
25export interface DfsSocial { label: string; href: string; icon: "github" | "x" | "discord" | "youtube" | "rss" }
26
27export interface DevFooterStatusProps {
28 brand?: string;
29 blurb?: string;
30 columns?: DfsColumn[];
31 regions?: DfsRegion[];
32 languages?: { value: string; label: string }[];
33 socials?: DfsSocial[];
34 legal?: { label: string; href: string }[];
35 build?: string;
36 onSubscribe?: (email: string) => Promise<void> | void;
37 className?: string;
38}
39
40const DEFAULT_COLUMNS: DfsColumn[] = [
41 { title: "Product", links: [{ label: "Queues", href: "#" }, { label: "Cron", href: "#" }, { label: "Workflows", href: "#", badge: "Beta" }, { label: "Rate limits", href: "#" }, { label: "Dead letters", href: "#" }, { label: "Pricing", href: "#" }] },
42 { title: "Developers", links: [{ label: "Docs", href: "#" }, { label: "API reference", href: "#" }, { label: "CLI", href: "#" }, { label: "TypeScript SDK", href: "#" }, { label: "Python SDK", href: "#" }, { label: "Go SDK", href: "#" }, { label: "Changelog", href: "#" }] },
43 { title: "Resources", links: [{ label: "Guides", href: "#" }, { label: "Templates", href: "#" }, { label: "From Sidekiq", href: "#" }, { label: "From Celery", href: "#" }, { label: "Benchmarks", href: "#" }, { label: "Blog", href: "#" }] },
44 { title: "Company", links: [{ label: "About", href: "#" }, { label: "Customers", href: "#" }, { label: "Careers", href: "#", badge: "4" }, { label: "Security", href: "#" }, { label: "Contact sales", href: "#" }] },
45 { title: "Community", links: [{ label: "Discord", href: "#" }, { label: "Discussions", href: "#" }, { label: "Office hours", href: "#" }, { label: "Open source", href: "#" }] },
46];
47
48const DEFAULT_REGIONS: DfsRegion[] = [
49 { code: "iad1", name: "Washington, D.C.", uptime: "99.998%", minor: [61] },
50 { code: "fra1", name: "Frankfurt", uptime: "100%" },
51 { code: "sin1", name: "Singapore", uptime: "99.981%", minor: [23, 24], incidents: [52] },
52 { code: "syd1", name: "Sydney", uptime: "100%" },
53 { code: "gru1", name: "São Paulo", uptime: "99.994%", minor: [80] },
54];
55
56const DEFAULT_LANGS = [
57 { value: "en", label: "English" },
58 { value: "de", label: "Deutsch" },
59 { value: "ja", label: "日本語" },
60 { value: "pt-BR", label: "Português (BR)" },
61 { value: "fr", label: "Français" },
62];
63
64const DEFAULT_SOCIALS: DfsSocial[] = [
65 { label: "GitHub", href: "#", icon: "github" },
66 { label: "X", href: "#", icon: "x" },
67 { label: "Discord", href: "#", icon: "discord" },
68 { label: "YouTube", href: "#", icon: "youtube" },
69 { label: "RSS feed", href: "#", icon: "rss" },
70];
71
72const DEFAULT_LEGAL = [
73 { label: "Terms", href: "#" },
74 { label: "Privacy", href: "#" },
75 { label: "DPA", href: "#" },
76 { label: "Subprocessors", href: "#" },
77 { label: "Cookie settings", href: "#" },
78];
79
80const ICONS: Record<DfsSocial["icon"], ReactNode> = {
81 github: (
82 <path fill="currentColor" d="M12 2.5a9.5 9.5 0 00-3 18.5c.5.1.6-.2.6-.5v-1.7c-2.6.6-3.2-1.2-3.2-1.2-.4-1.1-1-1.4-1-1.4-.9-.6 0-.6 0-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.1-.2-4.3-1-4.3-4.7 0-1 .4-1.9 1-2.6-.1-.2-.4-1.2.1-2.5 0 0 .8-.3 2.6 1a9 9 0 014.8 0c1.8-1.3 2.6-1 2.6-1 .5 1.3.2 2.3.1 2.5.6.7 1 1.6 1 2.6 0 3.7-2.2 4.5-4.3 4.7.3.3.6.9.6 1.8v2.6c0 .3.2.6.7.5A9.5 9.5 0 0012 2.5z" />
83 ),
84 x: <path fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" d="M5 4.5l14 15M19 4.5l-14 15" />,
85 discord: (
86 <g fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round">
87 <path d="M7.5 6.5c3-1.3 6-1.3 9 0 1.6 2.4 2.5 5.3 2.5 8.6-1.3 1.2-2.8 2-4.4 2.4l-1-1.7m-3.2 0l-1 1.7C8.3 17.1 6.8 16.3 5 15.1c0-3.3.9-6.2 2.5-8.6z" />
88 <circle cx="9.5" cy="12" r="1.1" fill="currentColor" stroke="none" />
89 <circle cx="14.5" cy="12" r="1.1" fill="currentColor" stroke="none" />
90 </g>
91 ),
92 youtube: (
93 <g>
94 <rect x="2.75" y="5.75" width="18.5" height="12.5" rx="4" fill="none" stroke="currentColor" strokeWidth="1.7" />
95 <path d="M10.2 9.4v5.2l4.5-2.6z" fill="currentColor" />
96 </g>
97 ),
98 rss: (
99 <g fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round">
100 <path d="M5 11a8 8 0 018 8M5 5.5A13.5 13.5 0 0118.5 19" />
101 <circle cx="6" cy="18" r="1.4" fill="currentColor" stroke="none" />
102 </g>
103 ),
104};
105
106export function DevFooterStatus({
107 brand = "Quay",
108 blurb = "Durable background jobs for TypeScript, Python and Go. Retries, schedules and fan-out without running a single Redis box.",
109 columns = DEFAULT_COLUMNS,
110 regions = DEFAULT_REGIONS,
111 languages = DEFAULT_LANGS,
112 socials = DEFAULT_SOCIALS,
113 legal = DEFAULT_LEGAL,
114 build = "v4.12.0 · main@8f3c2a1",
115 onSubscribe,
116 className,
117}: DevFooterStatusProps) {
118 const uid = useId();
119 const [open, setOpen] = useState(false);
120 const pillRef = useRef<HTMLButtonElement>(null);
121 const panelRef = useRef<HTMLDivElement>(null);
122 const [lang, setLang] = useState(languages[0]?.value ?? "en");
123 const [email, setEmail] = useState("");
124 const [err, setErr] = useState("");
125 const [phase, setPhase] = useState<"idle" | "busy" | "done">("idle");
126 const inputRef = useRef<HTMLInputElement>(null);
127
128 const degraded = regions.some((r) => r.incidents?.includes(89));
129
130 useEffect(() => {
131 if (!open) return;
132 const onDown = (e: PointerEvent) => {
133 const t = e.target as Node;
134 if (!panelRef.current?.contains(t) && !pillRef.current?.contains(t)) setOpen(false);
135 };
136 const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); pillRef.current?.focus(); } };
137 document.addEventListener("pointerdown", onDown);
138 document.addEventListener("keydown", onKey);
139 return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
140 }, [open]);
141
142 const validate = (v: string) => {
143 const s = v.trim();
144 if (!s) return "Enter an email address.";
145 if (!/^[^\s@]+@[^\s@]+$/.test(s)) return "An email needs one @, like ada@company.dev.";
146 if (!/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(s)) return `Add a domain ending, like ${s.split("@")[0]}@${s.split("@")[1] || "company"}.dev.`;
147 if (/@(mailinator|10minutemail|guerrillamail)\./i.test(s)) return "Disposable inboxes can't confirm. Use a real one.";
148 return "";
149 };
150
151 const submit = async (e: FormEvent) => {
152 e.preventDefault();
153 const m = validate(email);
154 setErr(m);
155 if (m) { inputRef.current?.focus(); return; }
156 setPhase("busy");
157 try {
158 await Promise.all([onSubscribe?.(email.trim()), new Promise((r) => setTimeout(r, 700))]);
159 setPhase("done");
160 } catch {
161 setPhase("idle");
162 setErr("Something went wrong on our side. Try again in a minute.");
163 }
164 };
165
166 return (
167 <footer className={`fd-dfs${className ? " " + className : ""}`}>
168 <style>{css}</style>
169 <div className="fd-dfs-in">
170 <div className="fd-dfs-grid">
171 <div className="fd-dfs-brand">
172 <a className="fd-dfs-logo" href="#" aria-label={`${brand} home`}>
173 <svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true">
174 <rect x="1" y="1" width="24" height="24" rx="6" fill="currentColor" />
175 <path d="M7 9h12M7 13h8M7 17h4" stroke="var(--fd-dfs-bg)" strokeWidth="2.2" strokeLinecap="round" />
176 <circle cx="18" cy="17" r="2" fill="var(--fd-dfs-accent)" />
177 </svg>
178 <span>{brand.toLowerCase()}</span>
179 </a>
180 <p className="fd-dfs-blurb">{blurb}</p>
181
182 <div className="fd-dfs-statuswrap">
183 <button
184 ref={pillRef}
185 type="button"
186 className="fd-dfs-pill"
187 data-state={degraded ? "warn" : "ok"}
188 aria-expanded={open}
189 aria-controls={`${uid}-st`}
190 onClick={() => setOpen((o) => !o)}
191 >
192 <span className="fd-dfs-dot" aria-hidden="true"><i /></span>
193 {degraded ? "Degraded performance" : "All systems normal"}
194 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true" className="fd-dfs-chev"><path d="M2.5 6.5L5 4l2.5 2.5" /></svg>
195 </button>
196 {open && (
197 <div ref={panelRef} id={`${uid}-st`} className="fd-dfs-panel" role="region" aria-label="System status by region">
198 <div className="fd-dfs-ph">
199 <span>Uptime, last 90 days</span>
200 <a href="#" className="fd-dfs-a">status.quay.dev</a>
201 </div>
202 <ul className="fd-dfs-regions">
203 {regions.map((r) => (
204 <li key={r.code}>
205 <div className="fd-dfs-rh">
206 <code>{r.code}</code>
207 <span>{r.name}</span>
208 <b>{r.uptime}</b>
209 </div>
210 <div className="fd-dfs-bars" aria-label={`${r.name}: ${r.uptime} uptime, ${(r.incidents?.length ?? 0) + (r.minor?.length ?? 0)} incident days`} role="img">
211 {Array.from({ length: 90 }, (_, d) => (
212 <i key={d} data-s={r.incidents?.includes(d) ? "down" : r.minor?.includes(d) ? "minor" : undefined} />
213 ))}
214 </div>
215 </li>
216 ))}
217 </ul>
218 <p className="fd-dfs-pf">Last incident: sin1 queue latency, 38 days ago, 14 min.</p>
219 </div>
220 )}
221 </div>
222
223 <ul className="fd-dfs-social">
224 {socials.map((s) => (
225 <li key={s.label}>
226 <a href={s.href} aria-label={s.label} className="fd-dfs-ico">
227 <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">{ICONS[s.icon]}</svg>
228 </a>
229 </li>
230 ))}
231 </ul>
232 </div>
233
234 <nav className="fd-dfs-cols" aria-label="Footer">
235 {columns.map((c) => (
236 <div key={c.title} className="fd-dfs-col">
237 <h3 className="fd-dfs-h">{c.title}</h3>
238 <ul>
239 {c.links.map((l) => (
240 <li key={l.label}>
241 <a className="fd-dfs-link" href={l.href}>{l.label}</a>
242 {l.badge && <span className="fd-dfs-badge">{l.badge}</span>}
243 </li>
244 ))}
245 </ul>
246 </div>
247 ))}
248 </nav>
249 </div>
250
251 <div className="fd-dfs-mid">
252 <form className="fd-dfs-news" onSubmit={submit} noValidate>
253 <div className="fd-dfs-nt">
254 <label htmlFor={`${uid}-em`} className="fd-dfs-h">Changelog by email</label>
255 <p>What shipped, every other Thursday. Plain text, no tracking pixels.</p>
256 </div>
257 {phase === "done" ? (
258 <div className="fd-dfs-done" role="status">
259 <span className="fd-dfs-check" aria-hidden="true">
260 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path className="fd-dfs-tick" d="M3 7.4l2.6 2.6L11 4.4" /></svg>
261 </span>
262 <div>
263 <p><b>Check {email.trim()}</b></p>
264 <p>The confirm link expires in 24 hours. <button type="button" className="fd-dfs-undo" onClick={() => { setPhase("idle"); setEmail(""); setTimeout(() => inputRef.current?.focus(), 0); }}>Use a different address</button></p>
265 </div>
266 </div>
267 ) : (
268 <div className="fd-dfs-fwrap">
269 <div className="fd-dfs-field" data-err={err ? "" : undefined}>
270 <input
271 ref={inputRef}
272 id={`${uid}-em`}
273 type="email"
274 inputMode="email"
275 autoComplete="email"
276 spellCheck={false}
277 placeholder="ada@company.dev"
278 value={email}
279 disabled={phase === "busy"}
280 aria-invalid={!!err}
281 aria-describedby={err ? `${uid}-er` : undefined}
282 onChange={(e) => { setEmail(e.target.value); if (err) setErr(""); }}
283 onBlur={() => { if (email.trim()) setErr(validate(email)); }}
284 />
285 <button type="submit" disabled={phase === "busy"} data-busy={phase === "busy" ? "" : undefined}>
286 <span>Subscribe</span>
287 {phase === "busy" && <i className="fd-dfs-spin" aria-hidden="true" />}
288 </button>
289 </div>
290 <p className="fd-dfs-err" id={`${uid}-er`} aria-live="polite">{err}</p>
291 </div>
292 )}
293 </form>
294
295 <div className="fd-dfs-lang">
296 <label htmlFor={`${uid}-lang`} className="fd-dfs-sr">Language</label>
297 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" aria-hidden="true" className="fd-dfs-globe"><circle cx="8" cy="8" r="6.2" /><path d="M1.8 8h12.4M8 1.8c1.8 1.7 2.7 3.8 2.7 6.2S9.8 12.5 8 14.2C6.2 12.5 5.3 10.4 5.3 8S6.2 3.5 8 1.8z" /></svg>
298 <select id={`${uid}-lang`} value={lang} onChange={(e) => setLang(e.target.value)}>
299 {languages.map((l) => <option key={l.value} value={l.value}>{l.label}</option>)}
300 </select>
301 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true" className="fd-dfs-sel"><path d="M2.5 4L5 6.5 7.5 4" /></svg>
302 </div>
303 </div>
304
305 <div className="fd-dfs-legal">
306 <p>© 2026 Quay Labs, Inc.</p>
307 <ul>{legal.map((l) => <li key={l.label}><a className="fd-dfs-link" href={l.href}>{l.label}</a></li>)}</ul>
308 <p className="fd-dfs-soc">SOC 2 Type II · GDPR · HIPAA on Scale</p>
309 <p className="fd-dfs-build"><span aria-hidden="true">◆</span> {build}</p>
310 </div>
311 </div>
312 </footer>
313 );
314}
315
316const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
317const MONO = `ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace`;
318
319const css = `
320:where(.fd-dfs,.fd-dfs *){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}
321:where(.fd-dfs,.fd-dfs *)::before,:where(.fd-dfs,.fd-dfs *)::after{box-sizing:border-box}
322:where(.fd-dfs svg){display:block;flex-shrink:0}
323:where(.fd-dfs button,.fd-dfs input,.fd-dfs select){appearance:none;-webkit-appearance:none;border-radius:0;outline:0}
324:where(.fd-dfs button){cursor:pointer}
325.fd-dfs{
326 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-ease-in-out:cubic-bezier(.65,0,.35,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;
327 --fd-dfs-bg:#FFFFFF;--fd-dfs-fg:#0B0B0C;--fd-dfs-mut:#66666E;--fd-dfs-line:#E8E8EA;--fd-dfs-soft:#F6F6F7;--fd-dfs-accent:#F04E23;--fd-dfs-ok:#13A150;--fd-dfs-warn:#E0A100;--fd-dfs-bad:#E5484D;
328 display:block;width:100%;background:var(--fd-dfs-bg);color:var(--fd-dfs-fg);border-top:1px solid var(--fd-dfs-line);
329 font:400 14px/20px ${SANS};letter-spacing:-.003em;-webkit-font-smoothing:antialiased}
330.fd-dfs-in{max-width:1280px;margin:0 auto;padding:56px clamp(16px,3vw,40px) 28px}
331.fd-dfs-grid{display:grid;grid-template-columns:minmax(260px,1.1fr) minmax(0,3fr);gap:40px clamp(32px,5vw,72px)}
332.fd-dfs-logo{display:inline-flex;align-items:center;gap:9px;font:700 22px/26px ${SANS};letter-spacing:-.045em;color:var(--fd-dfs-fg);border-radius:6px;outline:0}
333.fd-dfs-blurb{margin-top:16px;max-width:300px;color:var(--fd-dfs-mut)}
334.fd-dfs-statuswrap{position:relative;margin-top:20px}
335.fd-dfs-pill{display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 10px 0 9px;border-radius:9999px;background:var(--fd-dfs-soft);box-shadow:inset 0 0 0 1px var(--fd-dfs-line);font:500 13px/1 ${SANS};color:var(--fd-dfs-fg);transition:box-shadow var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-spring)}
336.fd-dfs-pill:hover{box-shadow:inset 0 0 0 1px #CFCFD4}
337.fd-dfs-pill:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}
338.fd-dfs-chev{color:var(--fd-dfs-mut);transform:rotate(180deg);transition:transform var(--fd-dur-2) var(--fd-ease-out)}
339.fd-dfs-pill[aria-expanded="true"] .fd-dfs-chev{transform:none}
340.fd-dfs-dot{position:relative;display:block;width:8px;height:8px}
341.fd-dfs-dot i,.fd-dfs-dot::before{position:absolute;inset:0;border-radius:9999px;background:var(--fd-dfs-ok)}
342.fd-dfs-dot::before{content:"";animation:fd-dfs-ping 2.4s var(--fd-ease-out) infinite}
343.fd-dfs-pill[data-state="warn"] .fd-dfs-dot i,.fd-dfs-pill[data-state="warn"] .fd-dfs-dot::before{background:var(--fd-dfs-warn)}
344.fd-dfs-dot i{display:block}
345@keyframes fd-dfs-ping{0%{transform:scale(1);opacity:.55}70%,100%{transform:scale(2.6);opacity:0}}
346.fd-dfs-panel{position:absolute;left:0;bottom:calc(100% + 10px);z-index:20;width:340px;max-width:calc(100vw - 32px);padding:14px 14px 12px;border-radius:14px;background:var(--fd-dfs-bg);box-shadow:0 0 0 1px var(--fd-dfs-line),0 18px 40px -12px rgba(11,11,12,.22),0 2px 6px rgba(11,11,12,.06);transform-origin:20px 100%;animation:fd-dfs-pop var(--fd-dur-2) var(--fd-ease-out)}
347@keyframes fd-dfs-pop{from{opacity:0;transform:translateY(6px) scale(.97)}}
348.fd-dfs-ph{display:flex;justify-content:space-between;align-items:center;font:500 11px/16px ${MONO};letter-spacing:.02em;color:var(--fd-dfs-mut)}
349.fd-dfs-ph .fd-dfs-a{color:var(--fd-dfs-fg);box-shadow:inset 0 -1px 0 var(--fd-dfs-line)}
350.fd-dfs-regions{display:grid;gap:10px;margin-top:12px}
351.fd-dfs-rh{display:grid;grid-template-columns:40px 1fr auto;gap:8px;align-items:baseline;font:400 12px/16px ${SANS}}
352.fd-dfs-rh code{font:500 11px/16px ${MONO};color:var(--fd-dfs-mut)}
353.fd-dfs-rh b{font:500 11px/16px ${MONO};color:var(--fd-dfs-fg);font-variant-numeric:tabular-nums}
354.fd-dfs-bars{display:grid;grid-template-columns:repeat(90,minmax(0,1fr));gap:1px;height:16px;margin-top:5px}
355.fd-dfs-bars i{display:block;border-radius:1px;background:var(--fd-dfs-ok);opacity:.82;transition:transform var(--fd-dur-1)}
356.fd-dfs-bars i[data-s="minor"]{background:var(--fd-dfs-warn);opacity:1}
357.fd-dfs-bars i[data-s="down"]{background:var(--fd-dfs-bad);opacity:1}
358.fd-dfs-bars i:hover{transform:scaleY(1.25)}
359.fd-dfs-pf{margin-top:12px;padding-top:10px;border-top:1px solid var(--fd-dfs-line);font:400 12px/16px ${SANS};color:var(--fd-dfs-mut)}
360.fd-dfs-social{display:flex;gap:4px;margin-top:24px;margin-left:-8px}
361.fd-dfs-ico{display:grid;place-items:center;width:34px;height:34px;border-radius:8px;color:var(--fd-dfs-mut);outline:0;transition:color var(--fd-dur-1),background-color var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-spring)}
362.fd-dfs-ico:hover{color:var(--fd-dfs-fg);background:var(--fd-dfs-soft)}
363.fd-dfs-ico:active{transform:scale(.92);transition-duration:var(--fd-dur-1)}
364.fd-dfs-cols{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:32px 20px}
365.fd-dfs-h{display:block;font:600 13px/18px ${SANS};color:var(--fd-dfs-fg)}
366.fd-dfs-col ul{display:grid;gap:9px;margin-top:14px}
367.fd-dfs-col li{display:flex;align-items:center;gap:6px;min-width:0}
368.fd-dfs-link{color:var(--fd-dfs-mut);border-radius:3px;outline:0;transition:color var(--fd-dur-1)}
369.fd-dfs-link:hover{color:var(--fd-dfs-fg)}
370.fd-dfs-badge{display:inline-flex;align-items:center;height:17px;padding:0 5px;border-radius:5px;font:500 10px/1 ${MONO};color:var(--fd-dfs-accent);background:color-mix(in srgb,var(--fd-dfs-accent) 9%,transparent)}
371.fd-dfs-link:focus-visible,.fd-dfs-ico:focus-visible,.fd-dfs-pill:focus-visible,.fd-dfs-logo:focus-visible,.fd-dfs-a:focus-visible,.fd-dfs-undo:focus-visible,.fd-dfs-field button:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-dfs-accent) 70%,transparent);outline-offset:2px}
372.fd-dfs-mid{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-top:48px;padding:24px 0;border-top:1px solid var(--fd-dfs-line);border-bottom:1px solid var(--fd-dfs-line)}
373.fd-dfs-news{display:flex;align-items:flex-start;gap:16px 40px;flex-wrap:wrap;flex:1;min-width:0}
374.fd-dfs-nt{max-width:280px}
375.fd-dfs-nt p{margin-top:2px;color:var(--fd-dfs-mut);font-size:13px;line-height:18px}
376.fd-dfs-fwrap{flex:1;min-width:260px;max-width:420px}
377.fd-dfs-field{display:flex;align-items:center;gap:4px;height:40px;padding:4px;border-radius:10px;background:var(--fd-dfs-bg);box-shadow:inset 0 0 0 1px #D9D9DD;transition:box-shadow var(--fd-dur-2)}
378.fd-dfs-field:focus-within{box-shadow:inset 0 0 0 1px var(--fd-dfs-fg),0 0 0 3px rgba(11,11,12,.07)}
379.fd-dfs-field[data-err]{box-shadow:inset 0 0 0 1px var(--fd-dfs-bad),0 0 0 3px color-mix(in srgb,var(--fd-dfs-bad) 14%,transparent);animation:fd-dfs-shake var(--fd-dur-3) var(--fd-ease-out)}
380@keyframes fd-dfs-shake{20%{transform:translateX(-4px)}45%{transform:translateX(3px)}70%{transform:translateX(-2px)}}
381.fd-dfs-field input{flex:1;min-width:0;height:32px;padding:0 8px;font:400 14px/20px ${MONO};letter-spacing:-.01em;color:var(--fd-dfs-fg);background:transparent}
382.fd-dfs-field input::placeholder{color:#A2A2A9}
383.fd-dfs-field button{position:relative;display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;border-radius:7px;background:var(--fd-dfs-fg);color:var(--fd-dfs-bg);font:500 13px/1 ${SANS};transition:transform var(--fd-dur-2) var(--fd-spring),opacity var(--fd-dur-1)}
384.fd-dfs-field button:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}
385.fd-dfs-field button[data-busy] span{opacity:0}
386.fd-dfs-spin{position:absolute;width:14px;height:14px;border-radius:9999px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;animation:fd-dfs-rot .7s linear infinite}
387@keyframes fd-dfs-rot{to{transform:rotate(360deg)}}
388.fd-dfs-err{min-height:18px;margin-top:6px;font:400 12px/18px ${SANS};color:#C8282E}
389.fd-dfs-done{display:flex;gap:12px;align-items:flex-start;flex:1;min-width:260px;max-width:440px;animation:fd-dfs-pop var(--fd-dur-3) var(--fd-ease-out)}
390.fd-dfs-done p{font-size:13px;line-height:19px;color:var(--fd-dfs-mut)}
391.fd-dfs-done b{font-weight:600;color:var(--fd-dfs-fg);overflow-wrap:anywhere}
392.fd-dfs-check{display:grid;place-items:center;flex:none;width:26px;height:26px;border-radius:9999px;background:var(--fd-dfs-ok);color:#fff;animation:fd-dfs-bump var(--fd-dur-3) var(--fd-spring)}
393@keyframes fd-dfs-bump{from{transform:scale(.4)}}
394.fd-dfs-tick{stroke-dasharray:14;stroke-dashoffset:14;animation:fd-dfs-draw var(--fd-dur-3) var(--fd-ease-out) 120ms forwards}
395@keyframes fd-dfs-draw{to{stroke-dashoffset:0}}
396.fd-dfs-undo{font:500 13px/19px ${SANS};color:var(--fd-dfs-fg);box-shadow:inset 0 -1px 0 #C9C9CE;border-radius:2px}
397.fd-dfs-lang{position:relative;display:flex;align-items:center;flex:none}
398.fd-dfs-lang select{height:34px;padding:0 30px 0 32px;border-radius:9999px;box-shadow:inset 0 0 0 1px var(--fd-dfs-line);background:var(--fd-dfs-bg);font:500 13px/1 ${SANS};color:var(--fd-dfs-fg);cursor:pointer;transition:box-shadow var(--fd-dur-1)}
399.fd-dfs-lang select:hover{box-shadow:inset 0 0 0 1px #CFCFD4}
400.fd-dfs-lang select:focus-visible{box-shadow:inset 0 0 0 1px var(--fd-dfs-fg),0 0 0 3px color-mix(in srgb,var(--fd-dfs-accent) 30%,transparent)}
401.fd-dfs-globe{position:absolute;left:11px;color:var(--fd-dfs-mut);pointer-events:none}
402.fd-dfs-sel{position:absolute;right:12px;color:var(--fd-dfs-mut);pointer-events:none}
403.fd-dfs-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
404.fd-dfs-legal{display:flex;align-items:center;gap:8px 24px;flex-wrap:wrap;padding-top:20px;font:400 12px/16px ${SANS};color:var(--fd-dfs-mut)}
405.fd-dfs-legal ul{display:flex;flex-wrap:wrap;gap:6px 18px}
406.fd-dfs-soc{margin-left:auto}
407.fd-dfs-build{font:400 11px/16px ${MONO};color:var(--fd-dfs-mut)}
408.fd-dfs-build span{color:var(--fd-dfs-accent);font-size:8px}
409@media (max-width:1100px){
410 .fd-dfs-grid{grid-template-columns:1fr}
411 .fd-dfs-cols{grid-template-columns:repeat(3,minmax(0,1fr))}
412}
413@media (max-width:640px){
414 .fd-dfs-cols{grid-template-columns:1fr 1fr}
415 .fd-dfs-mid{flex-direction:column}
416 .fd-dfs-news{flex-direction:column;width:100%}
417 .fd-dfs-fwrap,.fd-dfs-done{width:100%;max-width:none;min-width:0}
418 .fd-dfs-soc{margin-left:0}
419}
420@media (prefers-reduced-motion:reduce){.fd-dfs *{animation:none!important;transition-duration:1ms!important}.fd-dfs-tick{stroke-dashoffset:0}}
421@media (prefers-contrast:more){.fd-dfs{--fd-dfs-mut:#3A3A40;--fd-dfs-line:#9A9AA0}}
422`;
423
424/* ───────────────────────────── demo ───────────────────────────── */
425
426export function Demo() {
427 const [vote, setVote] = useState<null | "yes" | "no">(null);
428 return (
429 <div className="fd-dfsd">
430 <style>{demoCss}</style>
431 <section className="fd-dfsd-end" aria-label="End of article">
432 <div className="fd-dfsd-row">
433 <div className="fd-dfsd-help">
434 {vote ? (
435 <p role="status">{vote === "yes" ? "Thanks. We read every vote." : "Thanks. Tell us what was missing in #docs on Discord."}</p>
436 ) : (
437 <>
438 <p>Was this page helpful?</p>
439 <button type="button" onClick={() => setVote("yes")}>Yes</button>
440 <button type="button" onClick={() => setVote("no")}>No</button>
441 </>
442 )}
443 </div>
444 <p className="fd-dfsd-edit">Last edited 4 days ago · <a href="#">Edit on GitHub</a></p>
445 </div>
446 <div className="fd-dfsd-pager">
447 <a href="#"><span>Previous</span><b>Retries and backoff</b></a>
448 <a href="#" data-next=""><span>Next</span><b>Dead-letter queues</b></a>
449 </div>
450 </section>
451 <DevFooterStatus />
452 </div>
453 );
454}
455
456const demoCss = `
457:where(.fd-dfsd,.fd-dfsd *){box-sizing:border-box;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none}
458:where(.fd-dfsd button){appearance:none;-webkit-appearance:none;cursor:pointer;outline:0}
459.fd-dfsd{min-height:100vh;background:#FFFFFF;color:#0B0B0C;font:400 14px/20px ${SANS};-webkit-font-smoothing:antialiased}
460.fd-dfsd-end{max-width:760px;margin:0 auto;padding:48px 16px 64px}
461.fd-dfsd-row{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;padding-bottom:24px;border-bottom:1px solid #E8E8EA}
462.fd-dfsd-help{display:flex;align-items:center;gap:8px;min-height:30px;color:#66666E}
463.fd-dfsd-help button{height:28px;padding:0 12px;border-radius:9999px;box-shadow:inset 0 0 0 1px #E2E2E5;font-size:13px;color:#0B0B0C}
464.fd-dfsd-help button:hover{background:#F6F6F7}
465.fd-dfsd-help button:focus-visible,.fd-dfsd-pager a:focus-visible{outline:2px solid rgba(240,78,35,.7);outline-offset:2px}
466.fd-dfsd-edit{font-size:13px;color:#66666E}.fd-dfsd-edit a{color:#0B0B0C;box-shadow:inset 0 -1px 0 #D0D0D4}
467.fd-dfsd-pager{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:24px}
468.fd-dfsd-pager a{display:block;padding:16px 18px;border-radius:12px;box-shadow:inset 0 0 0 1px #E8E8EA;transition:box-shadow 120ms}
469.fd-dfsd-pager a:hover{box-shadow:inset 0 0 0 1px #0B0B0C}
470.fd-dfsd-pager a[data-next]{text-align:right}
471.fd-dfsd-pager span{display:block;font-size:12px;color:#66666E}
472.fd-dfsd-pager b{display:block;margin-top:2px;font-weight:600;font-size:15px}
473`;

More Footers. Same file contract.

Giant Wordmark Footer, footers preview: Footer with a giant wordmark: dark agency footer with sitemap and contacts, closed by a wordmark that rises…

Giant Wordmark Footer

Footer with a giant wordmark: dark agency footer with sitemap and contacts, closed by a wordmark that rises letter by letter.

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.

Questions about Developer Footer with Status

Run npx shadcn@latest add https://freedesignmd.com/r/dev-footer-status.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.

Developer footer with a live status pill and per-region uptime bars, sitemap, validated newsletter and language select. 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.

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.