Skip to content

Retail Footer

E-commerce footer with service promises that re-price per country, a currency picker, payment badges and phone accordions.

  • Footers
  • Fashion & retail
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Accordion height animation needs grid-template-rows interpolation (Chrome 107+, Safari 16+, Firefox 66+).

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/retail-footer.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/footers/retail-footer.tsx
1/**
2 * RetailFooter, a fashion e-commerce footer for a knitwear label. A row of
3 * service promises (the free-shipping threshold re-prices itself when you
4 * change country), sitemap columns that become accordions on phones, a
5 * country and currency picker, payment methods as typographic badges, and a
6 * large editorial sign-off line.
7 *
8 * Props: brand, promises, columns, countries ({ code, name, currency, rate
9 * from the base currency, step for rounding }), country / defaultCountry /
10 * onCountryChange (controlled or not), freeShippingFrom (base currency),
11 * payments, signoff ([before, emphasis, after]), legal.
12 * Helper export: priceIn(amount, country) formats a base-currency amount.
13 * CSS vars: --fd-rtf-bg, --fd-rtf-fg, --fd-rtf-accent.
14 *
15 * Browser support: all evergreen browsers. Accordion height animates with
16 * grid-template-rows (Chrome 107+, Safari 16+, Firefox 66+), older browsers snap.
17 * Accessibility: on narrow screens each column title is a button with
18 * aria-expanded/aria-controls; on wide screens it is a plain heading. The
19 * country picker is a button + listbox (arrow keys, Home/End, Enter, Esc,
20 * type-ahead not included). Reduced motion removes the number roll.
21 */
22import { useEffect, useId, useRef, useState } from "react";
23import type { KeyboardEvent, ReactNode } from "react";
24
25export interface RtfCountry { code: string; name: string; currency: string; rate: number; step: number; locale: string }
26export interface RtfColumn { title: string; links: { label: string; href: string }[] }
27export interface RtfPromise { icon: "returns" | "repair" | "ship" | "wool"; title: string; text: string }
28
29export interface RetailFooterProps {
30 brand?: string;
31 promises?: RtfPromise[];
32 columns?: RtfColumn[];
33 countries?: RtfCountry[];
34 country?: string;
35 defaultCountry?: string;
36 onCountryChange?: (code: string) => void;
37 freeShippingFrom?: number;
38 payments?: string[];
39 signoff?: [string, string, string];
40 legal?: string[];
41 className?: string;
42}
43
44export const COUNTRIES: RtfCountry[] = [
45 { code: "NO", name: "Norge", currency: "NOK", rate: 1, step: 10, locale: "nb-NO" },
46 { code: "SE", name: "Sverige", currency: "SEK", rate: 1.02, step: 10, locale: "sv-SE" },
47 { code: "DK", name: "Danmark", currency: "DKK", rate: 0.65, step: 5, locale: "da-DK" },
48 { code: "DE", name: "Deutschland", currency: "EUR", rate: 0.087, step: 5, locale: "de-DE" },
49 { code: "GB", name: "United Kingdom", currency: "GBP", rate: 0.074, step: 5, locale: "en-GB" },
50 { code: "US", name: "United States", currency: "USD", rate: 0.094, step: 5, locale: "en-US" },
51 { code: "JP", name: "日本", currency: "JPY", rate: 14.2, step: 100, locale: "ja-JP" },
52];
53
54export function priceIn(amount: number, c: RtfCountry) {
55 const v = Math.round((amount * c.rate) / c.step) * c.step;
56 return new Intl.NumberFormat(c.locale, { style: "currency", currency: c.currency, maximumFractionDigits: 0, minimumFractionDigits: 0 }).format(v);
57}
58const symbolOf = (c: RtfCountry) =>
59 new Intl.NumberFormat(c.locale, { style: "currency", currency: c.currency }).formatToParts(0).find((p) => p.type === "currency")?.value ?? c.currency;
60
61const DEFAULT_PROMISES: RtfPromise[] = [
62 { icon: "ship", title: "Free shipping from {free}", text: "Carbon-neutral, 2 to 4 days in Europe." },
63 { icon: "returns", title: "Free returns, 30 days", text: "Unworn, tags on. We send the label." },
64 { icon: "repair", title: "Repairs for life", text: "Moth hole, worn elbow: we darn it, free." },
65 { icon: "wool", title: "Knitted in Ålesund", text: "Norwegian Spælsau wool, traceable to the farm." },
66];
67
68const DEFAULT_COLUMNS: RtfColumn[] = [
69 { title: "Shop", links: [{ label: "Women", href: "#" }, { label: "Men", href: "#" }, { label: "Sweaters", href: "#" }, { label: "Scarves and hats", href: "#" }, { label: "Blankets", href: "#" }, { label: "Gift cards", href: "#" }] },
70 { title: "Help", links: [{ label: "Order status", href: "#" }, { label: "Returns and exchanges", href: "#" }, { label: "Size guide", href: "#" }, { label: "Caring for wool", href: "#" }, { label: "Contact us", href: "#" }] },
71 { title: "Repairs", links: [{ label: "Book a repair", href: "#" }, { label: "Darning kits", href: "#" }, { label: "The repair desk", href: "#" }, { label: "Worn again, second hand", href: "#" }] },
72 { title: "Løvstad", links: [{ label: "The mill", href: "#" }, { label: "Our farmers", href: "#" }, { label: "Journal", href: "#" }, { label: "Stores in Bergen and Oslo", href: "#" }, { label: "Careers", href: "#" }] },
73];
74
75const DEFAULT_PAYMENTS = ["Visa", "Mastercard", "Amex", "Klarna", "Vipps", "Apple Pay", "PayPal"];
76
77const ICON: Record<RtfPromise["icon"], ReactNode> = {
78 ship: <path d="M3 8h11v9H3zM14 11h4l3 3v3h-7M7 19.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM17 19.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" />,
79 returns: <path d="M9 7L5 11l4 4M5 11h10a4 4 0 010 8h-3" />,
80 repair: <path d="M16.5 3.5l4 4-11 11H5.5v-4zM13.5 6.5l4 4M4 21h16" />,
81 wool: <path d="M12 20a8 8 0 100-16 8 8 0 000 16zM6 9c4-1 9 1 12 5M5 13c4 0 8 2 10 6M9 4.8c3 1.5 6 5 7 9" />,
82};
83
84function useWide(q: string) {
85 const [wide, setWide] = useState<boolean | null>(null);
86 useEffect(() => {
87 const m = matchMedia(q);
88 const on = () => setWide(m.matches);
89 on();
90 m.addEventListener("change", on);
91 return () => m.removeEventListener("change", on);
92 }, [q]);
93 return wide;
94}
95
96export function RetailFooter({
97 brand = "Løvstad",
98 promises = DEFAULT_PROMISES,
99 columns = DEFAULT_COLUMNS,
100 countries = COUNTRIES,
101 country,
102 defaultCountry = "NO",
103 onCountryChange,
104 freeShippingFrom = 1500,
105 payments = DEFAULT_PAYMENTS,
106 signoff = ["Knitted slowly, ", "worn for thirty winters", "."],
107 legal = ["© 2026 Løvstad Strikk AS", "Org. nr. 921 448 302"],
108 className,
109}: RetailFooterProps) {
110 const uid = useId();
111 const [inner, setInner] = useState(defaultCountry);
112 const code = country ?? inner;
113 const cur = countries.find((c) => c.code === code) ?? countries[0];
114 const wide = useWide("(min-width: 768px)");
115 const [openCol, setOpenCol] = useState<Record<number, boolean>>({});
116 const [menu, setMenu] = useState(false);
117 const [active, setActive] = useState(0);
118 const btnRef = useRef<HTMLButtonElement>(null);
119 const listRef = useRef<HTMLUListElement>(null);
120 const boxRef = useRef<HTMLDivElement>(null);
121
122 const choose = (c: string) => {
123 if (country === undefined) setInner(c);
124 onCountryChange?.(c);
125 setMenu(false);
126 btnRef.current?.focus();
127 };
128
129 useEffect(() => {
130 if (!menu) return;
131 setActive(Math.max(0, countries.findIndex((c) => c.code === code)));
132 requestAnimationFrame(() => listRef.current?.focus());
133 const out = (e: PointerEvent) => { if (!boxRef.current?.contains(e.target as Node)) setMenu(false); };
134 document.addEventListener("pointerdown", out);
135 return () => document.removeEventListener("pointerdown", out);
136 }, [menu]); // eslint-disable-line react-hooks/exhaustive-deps
137
138 const onListKey = (e: KeyboardEvent) => {
139 const n = countries.length;
140 if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => (a + 1) % n); }
141 else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => (a - 1 + n) % n); }
142 else if (e.key === "Home") { e.preventDefault(); setActive(0); }
143 else if (e.key === "End") { e.preventDefault(); setActive(n - 1); }
144 else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); choose(countries[active].code); }
145 else if (e.key === "Escape" || e.key === "Tab") { setMenu(false); if (e.key === "Escape") btnRef.current?.focus(); }
146 };
147
148 const free = priceIn(freeShippingFrom, cur);
149
150 return (
151 <footer className={`fd-rtf${className ? " " + className : ""}`}>
152 <style>{css}</style>
153 <ul className="fd-rtf-promises" aria-label="Our promises">
154 {promises.map((p, i) => {
155 const parts = p.title.split("{free}");
156 return (
157 <li key={p.icon + i}>
158 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{ICON[p.icon]}</svg>
159 <div>
160 <p className="fd-rtf-pt">
161 {parts[0]}
162 {parts.length > 1 && <span className="fd-rtf-roll" key={cur.code}>{free}</span>}
163 {parts[1]}
164 </p>
165 <p className="fd-rtf-px">{p.text}</p>
166 </div>
167 </li>
168 );
169 })}
170 </ul>
171
172 <div className="fd-rtf-main">
173 <div className="fd-rtf-brand">
174 <p className="fd-rtf-word">{brand}</p>
175 <p className="fd-rtf-since">Strikkeri, Ålesund. Since 1931.</p>
176 <div className="fd-rtf-ctry" ref={boxRef}>
177 <p className="fd-rtf-lab" id={`${uid}-cl`}>Shipping to</p>
178 <button
179 ref={btnRef}
180 type="button"
181 className="fd-rtf-cbtn"
182 aria-haspopup="listbox"
183 aria-expanded={menu}
184 aria-labelledby={`${uid}-cl ${uid}-cv`}
185 onClick={() => setMenu((m) => !m)}
186 onKeyDown={(e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setMenu(true); } }}
187 >
188 <span id={`${uid}-cv`}><b>{cur.name}</b><span className="fd-rtf-cur">{cur.currency} {symbolOf(cur)}</span></span>
189 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.4" aria-hidden="true"><path d="M2.5 4L5 6.5 7.5 4" /></svg>
190 </button>
191 {menu && (
192 <ul
193 ref={listRef}
194 className="fd-rtf-list"
195 role="listbox"
196 tabIndex={-1}
197 aria-labelledby={`${uid}-cl`}
198 aria-activedescendant={`${uid}-o${active}`}
199 onKeyDown={onListKey}
200 >
201 {countries.map((c, i) => (
202 <li
203 key={c.code}
204 id={`${uid}-o${i}`}
205 role="option"
206 aria-selected={c.code === cur.code}
207 data-active={i === active ? "" : undefined}
208 onPointerEnter={() => setActive(i)}
209 onClick={() => choose(c.code)}
210 >
211 <span>{c.name}</span>
212 <span className="fd-rtf-cur">{c.currency}</span>
213 {c.code === cur.code && <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true"><path d="M2.5 6.2l2.3 2.3L9.5 3.8" /></svg>}
214 </li>
215 ))}
216 </ul>
217 )}
218 </div>
219 </div>
220
221 <nav className="fd-rtf-cols" aria-label="Footer">
222 {columns.map((c, i) => {
223 const isOpen = !!openCol[i];
224 const pid = `${uid}-p${i}`;
225 return (
226 <div className="fd-rtf-col" key={c.title} data-open={isOpen ? "" : undefined}>
227 {wide ? (
228 <h3 className="fd-rtf-ch">{c.title}</h3>
229 ) : (
230 <h3 className="fd-rtf-chw">
231 <button type="button" className="fd-rtf-ch fd-rtf-tog" aria-expanded={isOpen} aria-controls={pid} onClick={() => setOpenCol((o) => ({ ...o, [i]: !o[i] }))}>
232 {c.title}
233 <span className="fd-rtf-pm" aria-hidden="true"><i /><i /></span>
234 </button>
235 </h3>
236 )}
237 <div className="fd-rtf-panel" id={pid}>
238 <ul>
239 {c.links.map((l) => (
240 <li key={l.label}><a className="fd-rtf-a" href={l.href} tabIndex={wide === false && !isOpen ? -1 : undefined}>{l.label}</a></li>
241 ))}
242 </ul>
243 </div>
244 </div>
245 );
246 })}
247 </nav>
248 </div>
249
250 <p className="fd-rtf-sign">
251 {signoff[0]}<em>{signoff[1]}</em>{signoff[2]}
252 </p>
253
254 <div className="fd-rtf-base">
255 <ul className="fd-rtf-pay" aria-label="Payment methods">
256 {payments.map((p) => <li key={p}>{p}</li>)}
257 </ul>
258 <ul className="fd-rtf-legal">
259 {legal.map((l) => <li key={l}>{l}</li>)}
260 <li><a className="fd-rtf-a" href="#">Privacy</a></li>
261 <li><a className="fd-rtf-a" href="#">Terms of sale</a></li>
262 <li><a className="fd-rtf-a" href="#">Accessibility</a></li>
263 </ul>
264 </div>
265 </footer>
266 );
267}
268
269const SERIF = `"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif`;
270const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;
271
272const css = `
273:where(.fd-rtf,.fd-rtf *){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;font-style:normal}
274:where(.fd-rtf,.fd-rtf *)::before,:where(.fd-rtf,.fd-rtf *)::after{box-sizing:border-box}
275:where(.fd-rtf svg){display:block;flex-shrink:0}
276:where(.fd-rtf button){appearance:none;-webkit-appearance:none;border-radius:0;outline:0;cursor:pointer;text-align:inherit}
277.fd-rtf{
278 --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;
279 --fd-rtf-bg:#ECE5D8;--fd-rtf-fg:#231B15;--fd-rtf-mut:#6E6257;--fd-rtf-line:rgba(35,27,21,.16);--fd-rtf-accent:#9C3D1E;--fd-rtf-card:#F6F1E8;
280 display:block;width:100%;background:var(--fd-rtf-bg);color:var(--fd-rtf-fg);font:400 14px/20px ${SANS};letter-spacing:0;-webkit-font-smoothing:antialiased;overflow:hidden}
281.fd-rtf-promises{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border-bottom:1px solid var(--fd-rtf-line)}
282.fd-rtf-promises li{display:flex;gap:14px;align-items:flex-start;padding:24px clamp(16px,2.4vw,32px);border-left:1px solid var(--fd-rtf-line)}
283.fd-rtf-promises li:first-child{border-left:0}
284.fd-rtf-promises svg{color:var(--fd-rtf-accent);margin-top:-1px}
285.fd-rtf-pt{font:500 14px/20px ${SANS};color:var(--fd-rtf-fg)}
286.fd-rtf-px{margin-top:2px;font:400 13px/18px ${SANS};color:var(--fd-rtf-mut)}
287.fd-rtf-roll{display:inline-block;font-variant-numeric:tabular-nums;animation:fd-rtf-roll var(--fd-dur-3) var(--fd-ease-out)}
288@keyframes fd-rtf-roll{from{opacity:0;transform:translateY(60%);filter:blur(2px)}}
289.fd-rtf-main{display:grid;grid-template-columns:minmax(220px,1fr) minmax(0,3fr);gap:40px clamp(32px,5vw,80px);padding:56px clamp(16px,3.4vw,48px) 40px}
290.fd-rtf-word{font:400 40px/1 ${SERIF};letter-spacing:-.02em}
291.fd-rtf-since{margin-top:8px;font:italic 400 15px/22px ${SERIF};color:var(--fd-rtf-mut)}
292.fd-rtf-ctry{position:relative;margin-top:32px;max-width:260px}
293.fd-rtf-lab{font:500 11px/16px ${SANS};letter-spacing:.12em;text-transform:uppercase;color:var(--fd-rtf-mut)}
294.fd-rtf-cbtn{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;height:44px;margin-top:8px;padding:0 14px;border-radius:2px;box-shadow:inset 0 0 0 1px rgba(35,27,21,.3);color:var(--fd-rtf-fg);font:400 14px/20px ${SANS};transition:box-shadow var(--fd-dur-1)}
295.fd-rtf-cbtn:hover{box-shadow:inset 0 0 0 1px var(--fd-rtf-fg)}
296.fd-rtf-cbtn>span{display:flex;gap:10px;align-items:baseline;min-width:0}
297.fd-rtf-cbtn b{font-weight:500}
298.fd-rtf-cbtn svg{transition:transform var(--fd-dur-2) var(--fd-ease-out)}
299.fd-rtf-cbtn[aria-expanded="true"] svg{transform:rotate(180deg)}
300.fd-rtf-cur{font:400 12px/16px ${SANS};letter-spacing:.04em;color:var(--fd-rtf-mut)}
301.fd-rtf-list{position:absolute;left:0;right:0;bottom:calc(100% - 16px);z-index:10;padding:6px;background:var(--fd-rtf-card);box-shadow:0 0 0 1px var(--fd-rtf-line),0 20px 40px -16px rgba(35,27,21,.3);border-radius:2px;outline:0;animation:fd-rtf-pop var(--fd-dur-2) var(--fd-ease-out);transform-origin:50% 100%}
302@keyframes fd-rtf-pop{from{opacity:0;transform:translateY(6px)}}
303.fd-rtf-list li{display:grid;grid-template-columns:1fr auto 12px;gap:10px;align-items:center;height:36px;padding:0 10px;cursor:pointer;font:400 14px/20px ${SANS};border-radius:1px}
304.fd-rtf-list li[data-active]{background:rgba(35,27,21,.07)}
305.fd-rtf-list li[aria-selected="true"]{font-weight:500}
306.fd-rtf-list:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--fd-rtf-accent) 70%,transparent),0 20px 40px -16px rgba(35,27,21,.3)}
307.fd-rtf-cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:32px 24px}
308.fd-rtf-ch{display:block;font:500 11px/16px ${SANS};letter-spacing:.12em;text-transform:uppercase;color:var(--fd-rtf-mut)}
309.fd-rtf-chw{display:block}
310.fd-rtf-tog{display:flex;align-items:center;justify-content:space-between;width:100%;cursor:default}
311.fd-rtf-pm{display:none}
312.fd-rtf-panel{display:grid;grid-template-rows:1fr}
313.fd-rtf-panel>ul{min-height:0;overflow:hidden;display:grid;gap:9px;padding-top:16px}
314.fd-rtf-a{color:var(--fd-rtf-fg);border-radius:1px;outline:0;background:linear-gradient(currentColor,currentColor) 0 100%/0 1px no-repeat;transition:background-size var(--fd-dur-3) var(--fd-ease-out),color var(--fd-dur-1)}
315.fd-rtf-a:hover{background-size:100% 1px;color:var(--fd-rtf-accent)}
316.fd-rtf-a:focus-visible,.fd-rtf-cbtn:focus-visible,.fd-rtf-tog:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-rtf-accent) 70%,transparent);outline-offset:2px}
317.fd-rtf-sign{padding:8px clamp(16px,3.4vw,48px) 40px;font:400 clamp(2.5rem,7.6vw,7.5rem)/.98 ${SERIF};letter-spacing:-.035em;color:var(--fd-rtf-fg);text-wrap:balance}
318.fd-rtf-sign em{font-style:italic;color:var(--fd-rtf-accent)}
319.fd-rtf-base{display:flex;justify-content:space-between;align-items:center;gap:16px 32px;flex-wrap:wrap;margin:0 clamp(16px,3.4vw,48px);padding:18px 0 24px;border-top:1px solid var(--fd-rtf-line)}
320.fd-rtf-pay{display:flex;flex-wrap:wrap;gap:6px}
321.fd-rtf-pay li{display:inline-flex;align-items:center;height:24px;padding:0 8px;border-radius:3px;box-shadow:inset 0 0 0 1px rgba(35,27,21,.22);font:600 10px/1 ${SANS};letter-spacing:.08em;text-transform:uppercase;color:var(--fd-rtf-fg);background:var(--fd-rtf-card)}
322.fd-rtf-legal{display:flex;flex-wrap:wrap;gap:6px 20px;font:400 12px/16px ${SANS};color:var(--fd-rtf-mut)}
323.fd-rtf-legal .fd-rtf-a{color:var(--fd-rtf-mut)}
324@media (max-width:1020px){
325 .fd-rtf-promises{grid-template-columns:1fr 1fr}
326 .fd-rtf-promises li:nth-child(3){border-left:0}
327 .fd-rtf-promises li:nth-child(n+3){border-top:1px solid var(--fd-rtf-line)}
328 .fd-rtf-main{grid-template-columns:1fr}
329}
330@media (max-width:767px){
331 .fd-rtf-promises{grid-template-columns:1fr}
332 .fd-rtf-promises li{border-left:0;padding:16px}
333 .fd-rtf-promises li+li{border-top:1px solid var(--fd-rtf-line)}
334 .fd-rtf-cols{grid-template-columns:1fr;gap:0;border-top:1px solid var(--fd-rtf-line)}
335 .fd-rtf-col{border-bottom:1px solid var(--fd-rtf-line)}
336 .fd-rtf-tog{height:52px;cursor:pointer;font-size:12px;color:var(--fd-rtf-fg)}
337 .fd-rtf-pm{position:relative;display:block;width:12px;height:12px}
338 .fd-rtf-pm i{position:absolute;left:0;top:5.5px;width:12px;height:1.2px;background:currentColor;transition:transform var(--fd-dur-3) var(--fd-ease-out)}
339 .fd-rtf-pm i:last-child{transform:rotate(90deg)}
340 .fd-rtf-col[data-open] .fd-rtf-pm i:last-child{transform:rotate(0)}
341 .fd-rtf-panel{grid-template-rows:0fr;transition:grid-template-rows var(--fd-dur-3) var(--fd-ease-out)}
342 .fd-rtf-col[data-open] .fd-rtf-panel{grid-template-rows:1fr}
343 .fd-rtf-panel>ul{padding-top:0}
344 .fd-rtf-col[data-open] .fd-rtf-panel>ul{padding-bottom:18px}
345 .fd-rtf-ctry{max-width:none}
346 .fd-rtf-main{padding-top:40px}
347}
348@media (prefers-reduced-motion:reduce){.fd-rtf *{animation:none!important;transition-duration:1ms!important}}
349@media (prefers-contrast:more){.fd-rtf{--fd-rtf-mut:#43392F;--fd-rtf-line:rgba(35,27,21,.5)}}
350`;
351
352/* ───────────────────────────── demo ───────────────────────────── */
353
354const RECENT = [
355 { name: "Fisherman rib scarf", colour: "Havre", nok: 1290, src: "/img/lux/drape-oat.webp" },
356 { name: "Selbu crew neck", colour: "Rust", nok: 3490, src: "/img/lux/drape-terra.webp" },
357 { name: "Fjordline cardigan", colour: "Natt", nok: 3990, src: "/img/lux/drape-ink.webp" },
358];
359
360export function Demo() {
361 const [code, setCode] = useState("NO");
362 const c = COUNTRIES.find((x) => x.code === code) ?? COUNTRIES[0];
363 return (
364 <div className="fd-rtfd">
365 <style>{demoCss}</style>
366 <section className="fd-rtfd-recent" aria-label="Recently viewed">
367 <div className="fd-rtfd-h">
368 <h2>Recently viewed</h2>
369 <a href="#">Clear</a>
370 </div>
371 <ul>
372 {RECENT.map((r) => (
373 <li key={r.name}>
374 <a href="#">
375 <img src={r.src} alt={`${r.name} in ${r.colour}, close-up of the knit`} width={600} height={750} loading="lazy" decoding="async" />
376 <span className="fd-rtfd-n">{r.name}</span>
377 <span className="fd-rtfd-m">{r.colour} <b key={code}>{priceIn(r.nok, c)}</b></span>
378 </a>
379 </li>
380 ))}
381 </ul>
382 </section>
383 <RetailFooter country={code} onCountryChange={setCode} />
384 </div>
385 );
386}
387
388const demoCss = `
389:where(.fd-rtfd,.fd-rtfd *){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}
390:where(.fd-rtfd img){display:block}
391.fd-rtfd{min-height:100vh;background:#F6F1E8;color:#231B15;font:400 14px/20px ${SANS};-webkit-font-smoothing:antialiased}
392.fd-rtfd-recent{padding:48px clamp(16px,3.4vw,48px) 56px}
393.fd-rtfd-h{display:flex;justify-content:space-between;align-items:baseline}
394.fd-rtfd-h h2{font:400 28px/32px ${SERIF};letter-spacing:-.015em}
395.fd-rtfd-h a{font-size:13px;color:#6E6257;box-shadow:inset 0 -1px 0 rgba(35,27,21,.3)}
396.fd-rtfd-recent ul{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:20px}
397.fd-rtfd-recent a{display:block;outline:0;border-radius:2px}
398.fd-rtfd-recent a:focus-visible{outline:2px solid rgba(156,61,30,.7);outline-offset:2px}
399.fd-rtfd-recent img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;transition:transform 700ms cubic-bezier(.22,1,.36,1)}
400.fd-rtfd-recent li{overflow:hidden}
401.fd-rtfd-recent a:hover img{transform:scale(1.03)}
402.fd-rtfd-n{display:block;margin-top:12px;font-weight:500}
403.fd-rtfd-m{display:flex;justify-content:space-between;color:#6E6257}
404.fd-rtfd-m b{font-weight:400;color:#231B15;font-variant-numeric:tabular-nums;animation:fd-rtfd-in 420ms cubic-bezier(.22,1,.36,1)}
405@keyframes fd-rtfd-in{from{opacity:0;transform:translateY(4px)}}
406@media (max-width:767px){.fd-rtfd-recent ul{grid-template-columns:1fr 1fr}.fd-rtfd-recent li:last-child{display:none}.fd-rtfd-recent img{aspect-ratio:4/5}}
407@media (prefers-reduced-motion:reduce){.fd-rtfd *{animation:none!important;transition:none!important}}
408`;

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 Retail Footer

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

E-commerce footer with service promises that re-price per country, a currency picker, payment badges and phone accordions. 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. Accordion height animation needs grid-template-rows interpolation (Chrome 107+, Safari 16+, Firefox 66+).

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.