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
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 of3 * service promises (the free-shipping threshold re-prices itself when you4 * change country), sitemap columns that become accordions on phones, a5 * country and currency picker, payment methods as typographic badges, and a6 * large editorial sign-off line.7 *8 * Props: brand, promises, columns, countries ({ code, name, currency, rate9 * 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 with16 * grid-template-rows (Chrome 107+, Safari 16+, Firefox 66+), older browsers snap.17 * Accessibility: on narrow screens each column title is a button with18 * aria-expanded/aria-controls; on wide screens it is a plain heading. The19 * 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-deps137 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 <button179 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 <ul193 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 <li203 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.
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.


