Curtain Reveal Footer
Curtain reveal footer fixed beneath the page and uncovered as content lifts away, with tables, hours, address and a Book call.
- Footers
- Food & beverage
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers (clip-path: inset). Falls back to a static footer when it is taller than the viewport.
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/curtain-reveal-footer.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/footers/curtain-reveal-footer.tsx
1/**2 * CurtainRevealFooter, a restaurant footer that sits still while the page3 * lifts off it like a curtain. Tonight's free tables are chips you can pick,4 * a party-size stepper and a huge serif "Book a table" that turns into a5 * confirmation. Below: hours, address, a small terrace photo, wordmark.6 *7 * Technique (works anywhere you paste it): the footer is `position: fixed` to8 * the bottom of the viewport inside a wrapper that has `clip-path: inset(0)`.9 * The wrapper takes the footer's measured height, so it sits in normal flow10 * after your content; the clip makes the fixed layer visible only through the11 * wrapper's box, which is what makes the content appear to lift. No z-index12 * or background changes are needed on the content above. If the footer is13 * taller than the viewport (small phones), it falls back to a normal static14 * footer so nothing is ever unreachable.15 *16 * Props: name, slots, hours, address, phone, note, photo { src, alt },17 * legal, onBook(slot, guests).18 * CSS vars: --fd-crf-bg, --fd-crf-fg, --fd-crf-accent.19 *20 * Browser support: clip-path inset (all evergreen browsers).21 * Accessibility: slots are a radio group (arrow keys move), the stepper uses22 * labelled buttons and an output, the booking confirmation is announced via23 * a polite live region. Reduced motion removes the depth shading and scale.24 * SSR: renders static; the curtain switches on after measuring in an effect.25 */26import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";27import type { KeyboardEvent } from "react";28 29export interface CrfSlot { time: string; left: number }30export interface CrfHours { label: string; time: string; days: string }31 32export interface CurtainRevealFooterProps {33 name?: string;34 slots?: CrfSlot[];35 hours?: CrfHours[];36 address?: string[];37 phone?: string;38 note?: string;39 photo?: { src: string; alt: string };40 legal?: string[];41 onBook?: (slot: string, guests: number) => void;42 className?: string;43}44 45const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;46 47const DEFAULT_SLOTS: CrfSlot[] = [48 { time: "19:30", left: 0 },49 { time: "20:15", left: 2 },50 { time: "21:00", left: 4 },51 { time: "21:45", left: 1 },52];53const DEFAULT_HOURS: CrfHours[] = [54 { label: "Pranzo", time: "12:30 to 15:00", days: "Wed to Sun" },55 { label: "Cena", time: "19:30 to 23:00", days: "Daily, not Tuesday" },56 { label: "Bar", time: "from 17:00", days: "Spritz on the terrace" },57];58 59export function CurtainRevealFooter({60 name = "Limonaia",61 slots = DEFAULT_SLOTS,62 hours = DEFAULT_HOURS,63 address = ["Via Pasitea 148", "84017 Positano SA, Italia"],64 phone = "+39 089 875 0142",65 note = "Ten minutes up from the beach. Take the steps, or the blue Interno bus to the Chiesa Nuova stop.",66 photo = { src: "/img/halcyon/pool-lemons-480.webp", alt: "Lemon trees over the terrace and the bay of Positano" },67 legal = ["© 2026 Limonaia di Positano S.r.l.", "P.IVA 04827310657"],68 onBook,69 className,70}: CurtainRevealFooterProps) {71 const wrapRef = useRef<HTMLDivElement>(null);72 const layerRef = useRef<HTMLDivElement>(null);73 const [h, setH] = useState<number | null>(null);74 const [fixed, setFixed] = useState(false);75 const firstOpen = slots.find((s) => s.left > 0)?.time ?? "";76 const [slot, setSlot] = useState(firstOpen);77 const [guests, setGuests] = useState(2);78 const [booked, setBooked] = useState<null | { slot: string; guests: number }>(null);79 const slotRefs = useRef<(HTMLButtonElement | null)[]>([]);80 81 useIso(() => {82 const layer = layerRef.current;83 if (!layer) return;84 const measure = () => {85 const lh = layer.offsetHeight;86 setH(lh);87 setFixed(lh <= window.innerHeight - 24);88 };89 measure();90 const ro = new ResizeObserver(measure);91 ro.observe(layer);92 window.addEventListener("resize", measure);93 return () => { ro.disconnect(); window.removeEventListener("resize", measure); };94 }, []);95 96 // depth: the footer brightens and settles as the curtain lifts97 useEffect(() => {98 const wrap = wrapRef.current, layer = layerRef.current;99 if (!wrap || !layer || !fixed) { layer?.style.removeProperty("--fd-crf-p"); return; }100 if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;101 let raf = 0;102 const tick = () => {103 raf = 0;104 const r = wrap.getBoundingClientRect();105 const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / r.height));106 layer.style.setProperty("--fd-crf-p", p.toFixed(3));107 };108 const on = () => { if (!raf) raf = requestAnimationFrame(tick); };109 tick();110 window.addEventListener("scroll", on, { passive: true });111 window.addEventListener("resize", on);112 return () => { window.removeEventListener("scroll", on); window.removeEventListener("resize", on); cancelAnimationFrame(raf); };113 }, [fixed]);114 115 const open = slots.filter((s) => s.left > 0);116 const onSlotKey = (e: KeyboardEvent, i: number) => {117 if (!["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"].includes(e.key)) return;118 e.preventDefault();119 const dir = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : -1;120 for (let k = 1; k <= slots.length; k++) {121 const j = (i + dir * k + slots.length) % slots.length;122 if (slots[j].left > 0) { setSlot(slots[j].time); slotRefs.current[j]?.focus(); break; }123 }124 };125 const maxGuests = 8;126 const uid = useId();127 const book = () => {128 if (!slot) return;129 setBooked({ slot, guests });130 onBook?.(slot, guests);131 };132 133 return (134 <div135 ref={wrapRef}136 className={`fd-crf${className ? " " + className : ""}`}137 data-fixed={fixed ? "" : undefined}138 style={fixed && h ? { height: `${h}px` } : undefined}139 >140 <style>{css}</style>141 <footer ref={layerRef} className="fd-crf-layer">142 <div className="fd-crf-in">143 <div className="fd-crf-book">144 <div className="fd-crf-bl">145 <p className="fd-crf-k">Tonight, {open.length ? `${open.reduce((a, s) => a + s.left, 0)} tables left` : "fully booked"}</p>146 {booked ? (147 <p className="fd-crf-big fd-crf-done">148 <em>Grazie.</em> {booked.guests} {booked.guests === 1 ? "guest" : "guests"}, {booked.slot}.149 </p>150 ) : (151 <button type="button" className="fd-crf-big fd-crf-cta" onClick={book} disabled={!slot}>152 <em>Book</em> a table153 <svg className="fd-crf-arrow" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true"><path d="M10 32h42M36 16l16 16-16 16" /></svg>154 </button>155 )}156 <p className="fd-crf-live" role="status" aria-live="polite">157 {booked ? (158 <>We are holding it for 15 minutes and will text you to confirm. <button type="button" className="fd-crf-link" onClick={() => setBooked(null)}>Change</button></>159 ) : (160 "Larger party, or a birthday? Call us and we will move the lemons."161 )}162 </p>163 </div>164 <div className="fd-crf-pick" aria-disabled={booked ? "true" : undefined}>165 <p className="fd-crf-k" id={`${uid}-t`}>Time</p>166 <div className="fd-crf-slots" role="radiogroup" aria-labelledby={`${uid}-t`}>167 {slots.map((s, i) => {168 const on = slot === s.time;169 const full = s.left === 0;170 return (171 <button172 key={s.time}173 ref={(el) => { slotRefs.current[i] = el; }}174 type="button"175 role="radio"176 aria-checked={on}177 aria-disabled={full || !!booked}178 tabIndex={on || (!slot && i === 0) ? 0 : -1}179 className="fd-crf-slot"180 data-on={on ? "" : undefined}181 data-full={full ? "" : undefined}182 onClick={() => !full && !booked && setSlot(s.time)}183 onKeyDown={(e) => !booked && onSlotKey(e, i)}184 >185 <span className="fd-crf-st">{s.time}</span>186 <span className="fd-crf-sl">{full ? "Full" : s.left === 1 ? "Last table" : `${s.left} left`}</span>187 </button>188 );189 })}190 </div>191 <div className="fd-crf-guests">192 <p className="fd-crf-k" id={`${uid}-g`}>Guests</p>193 <div className="fd-crf-step" role="group" aria-labelledby={`${uid}-g`}>194 <button type="button" aria-label="One guest fewer" disabled={guests <= 1 || !!booked} onClick={() => setGuests((g) => Math.max(1, g - 1))}>195 <svg width="14" height="14" viewBox="0 0 14 14" stroke="currentColor" strokeWidth="1.6" aria-hidden="true"><path d="M2 7h10" /></svg>196 </button>197 <output aria-live="polite" className="fd-crf-n">{guests}</output>198 <button type="button" aria-label="One guest more" disabled={guests >= maxGuests || !!booked} onClick={() => setGuests((g) => Math.min(maxGuests, g + 1))}>199 <svg width="14" height="14" viewBox="0 0 14 14" stroke="currentColor" strokeWidth="1.6" aria-hidden="true"><path d="M2 7h10M7 2v10" /></svg>200 </button>201 </div>202 </div>203 </div>204 </div>205 206 <div className="fd-crf-info">207 <div className="fd-crf-col">208 <h3 className="fd-crf-k">Orari</h3>209 <dl className="fd-crf-hours">210 {hours.map((x) => (211 <div key={x.label}>212 <dt>{x.label}</dt>213 <dd><span>{x.time}</span><small>{x.days}</small></dd>214 </div>215 ))}216 </dl>217 </div>218 <div className="fd-crf-col">219 <h3 className="fd-crf-k">Find us</h3>220 <address className="fd-crf-addr">221 {address.map((l) => <span key={l}>{l}</span>)}222 <a className="fd-crf-a" href={`tel:${phone.replace(/\s/g, "")}`}>{phone}</a>223 </address>224 <p className="fd-crf-note">{note}</p>225 </div>226 <figure className="fd-crf-fig">227 <img src={photo.src} alt={photo.alt} width={480} height={360} loading="lazy" decoding="async" />228 <figcaption>The terrace. Six tables, walk-ins after 22:00.</figcaption>229 </figure>230 </div>231 232 <div className="fd-crf-base">233 <span className="fd-crf-word" aria-label={name}>{name}<i aria-hidden="true">.</i></span>234 <ul className="fd-crf-legal">235 {legal.map((l) => <li key={l}>{l}</li>)}236 <li><a className="fd-crf-a" href="#">Instagram</a></li>237 <li><a className="fd-crf-a" href="#">Allergens</a></li>238 </ul>239 </div>240 </div>241 </footer>242 </div>243 );244}245 246const SERIF = `"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif`;247const SANS = `ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif`;248 249const css = `250:where(.fd-crf,.fd-crf *){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}251:where(.fd-crf,.fd-crf *)::before,:where(.fd-crf,.fd-crf *)::after{box-sizing:border-box}252:where(.fd-crf svg,.fd-crf img){display:block;flex-shrink:0}253:where(.fd-crf button){appearance:none;-webkit-appearance:none;cursor:pointer;text-align:inherit;border-radius:0}254.fd-crf{255 --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;256 --fd-crf-bg:#2A1310;--fd-crf-fg:#F4EAD5;--fd-crf-mut:#C2A991;--fd-crf-line:rgba(244,234,213,.16);--fd-crf-accent:#EBC64B;257 position:relative;width:100%;clip-path:inset(0);258 font:400 15px/22px ${SANS};color:var(--fd-crf-fg);-webkit-font-smoothing:antialiased}259.fd-crf::after{content:"";position:absolute;inset:0 0 auto;height:56px;background:linear-gradient(rgba(0,0,0,.42),transparent);pointer-events:none;z-index:2}260.fd-crf-layer{display:block;width:100%;background:var(--fd-crf-bg);color:var(--fd-crf-fg)}261.fd-crf[data-fixed] .fd-crf-layer{position:fixed;left:0;right:0;bottom:0}262.fd-crf-in{max-width:1440px;margin:0 auto;padding:clamp(48px,6vw,80px) clamp(16px,4vw,56px) 24px;263 transform:translateY(calc((1 - var(--fd-crf-p,1)) * -48px)) scale(calc(.97 + var(--fd-crf-p,1) * .03));transform-origin:50% 100%;264 opacity:calc(.35 + var(--fd-crf-p,1) * .65)}265.fd-crf-k{display:block;font:600 11px/16px ${SANS};letter-spacing:.14em;text-transform:uppercase;color:var(--fd-crf-mut)}266.fd-crf-book{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:40px clamp(32px,5vw,80px);align-items:end;padding-bottom:clamp(36px,4vw,56px);border-bottom:1px solid var(--fd-crf-line)}267.fd-crf-big{display:flex;align-items:center;gap:.18em;margin-top:14px;font:400 clamp(3.5rem,9vw,8.5rem)/.92 ${SERIF};letter-spacing:-.035em;color:var(--fd-crf-fg)}268.fd-crf-big em{font-style:italic;color:var(--fd-crf-accent)}269.fd-crf-cta{outline:0;border-radius:6px;transition:color var(--fd-dur-2)}270.fd-crf-arrow{width:.62em;height:.62em;margin-left:.08em;transition:transform var(--fd-dur-3) var(--fd-spring)}271.fd-crf-cta:hover .fd-crf-arrow{transform:translateX(.12em)}272.fd-crf-cta:active{transform:scale(.985);transition:transform var(--fd-dur-1)}273.fd-crf-cta:focus-visible,.fd-crf-slot:focus-visible,.fd-crf-step button:focus-visible,.fd-crf-a:focus-visible,.fd-crf-link:focus-visible{outline:2px solid color-mix(in srgb,var(--fd-crf-accent) 70%,transparent);outline-offset:2px}274.fd-crf-cta:disabled{opacity:.4;cursor:not-allowed}275.fd-crf-done{display:block;font-size:clamp(2.75rem,6.4vw,6rem);line-height:1;animation:fd-crf-in var(--fd-dur-4) var(--fd-ease-out)}276@keyframes fd-crf-in{from{opacity:0;transform:translateY(.25em)}}277.fd-crf-live{margin-top:18px;max-width:440px;font:400 15px/22px ${SANS};color:var(--fd-crf-mut)}278.fd-crf-link{font:600 15px/22px ${SANS};color:var(--fd-crf-accent);box-shadow:inset 0 -1px 0 currentColor;outline:0;border-radius:2px}279.fd-crf-pick{display:grid;gap:12px}280.fd-crf-pick[aria-disabled="true"]{opacity:.45;pointer-events:none}281.fd-crf-slots{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}282.fd-crf-slot{display:flex;flex-direction:column;gap:2px;padding:10px 12px;border-radius:10px;box-shadow:inset 0 0 0 1px var(--fd-crf-line);color:var(--fd-crf-fg);outline:0;transition:background-color var(--fd-dur-1),box-shadow var(--fd-dur-2),transform var(--fd-dur-2) var(--fd-spring)}283.fd-crf-slot:hover:not([data-full]){box-shadow:inset 0 0 0 1px rgba(244,234,213,.4)}284.fd-crf-slot:active:not([data-full]){transform:scale(.96);transition-duration:var(--fd-dur-1)}285.fd-crf-slot[data-on]{background:var(--fd-crf-accent);color:#2A1310;box-shadow:none}286.fd-crf-slot[data-full]{cursor:not-allowed;color:rgba(244,234,213,.38)}287.fd-crf-slot[data-full] .fd-crf-st{text-decoration:line-through;text-decoration-thickness:1px}288.fd-crf-st{display:block;font:500 17px/22px ${SANS};letter-spacing:-.01em;font-variant-numeric:tabular-nums}289.fd-crf-sl{display:block;font:500 11px/14px ${SANS};opacity:.75}290.fd-crf-guests{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px;padding-top:12px;border-top:1px solid var(--fd-crf-line)}291.fd-crf-step{display:flex;align-items:center;gap:4px}292.fd-crf-step button{display:grid;place-items:center;width:36px;height:36px;border-radius:9999px;box-shadow:inset 0 0 0 1px var(--fd-crf-line);color:var(--fd-crf-fg);outline:0;transition:transform var(--fd-dur-2) var(--fd-spring),box-shadow var(--fd-dur-1)}293.fd-crf-step button:hover:not(:disabled){box-shadow:inset 0 0 0 1px rgba(244,234,213,.45)}294.fd-crf-step button:active:not(:disabled){transform:scale(.92);transition-duration:var(--fd-dur-1)}295.fd-crf-step button:disabled{opacity:.3;cursor:not-allowed}296.fd-crf-n{display:block;min-width:40px;text-align:center;font:400 1.5rem/1 ${SERIF};font-variant-numeric:tabular-nums}297.fd-crf-info{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,.8fr);gap:32px clamp(24px,4vw,64px);padding:36px 0}298.fd-crf-col .fd-crf-k{margin-bottom:14px}299.fd-crf-hours{display:grid;gap:12px}300.fd-crf-hours div{display:grid;grid-template-columns:72px 1fr;gap:12px;align-items:baseline}301.fd-crf-hours dt{font:italic 400 19px/24px ${SERIF};color:var(--fd-crf-accent)}302.fd-crf-hours dd{display:flex;flex-direction:column;font-variant-numeric:tabular-nums}303.fd-crf-hours dd span{font:400 15px/22px ${SANS};color:var(--fd-crf-fg)}304.fd-crf-hours small{font:400 13px/18px ${SANS};color:var(--fd-crf-mut)}305.fd-crf-addr{display:flex;flex-direction:column;font:400 19px/26px ${SERIF};color:var(--fd-crf-fg)}306.fd-crf-addr .fd-crf-a{margin-top:6px;align-self:flex-start;font:400 15px/22px ${SANS};font-variant-numeric:tabular-nums}307.fd-crf-a{color:var(--fd-crf-fg);box-shadow:inset 0 -1px 0 rgba(244,234,213,.35);border-radius:1px;outline:0;transition:color var(--fd-dur-1),box-shadow var(--fd-dur-2)}308.fd-crf-a:hover{color:var(--fd-crf-accent);box-shadow:inset 0 -1px 0 var(--fd-crf-accent)}309.fd-crf-note{margin-top:12px;max-width:320px;font:400 13px/19px ${SANS};color:var(--fd-crf-mut)}310.fd-crf-fig{display:block;justify-self:end;width:100%;max-width:280px}311.fd-crf-fig img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;border-radius:3px;filter:saturate(.9)}312.fd-crf-fig figcaption{margin-top:8px;font:italic 400 13px/18px ${SERIF};color:var(--fd-crf-mut)}313.fd-crf-base{display:flex;justify-content:space-between;align-items:flex-end;gap:16px 32px;flex-wrap:wrap;padding-top:20px;border-top:1px solid var(--fd-crf-line)}314.fd-crf-word{font:italic 400 32px/1 ${SERIF};letter-spacing:-.02em;color:var(--fd-crf-fg)}315.fd-crf-word i{color:var(--fd-crf-accent);font-style:normal}316.fd-crf-legal{display:flex;flex-wrap:wrap;gap:6px 20px;font:400 12px/16px ${SANS};color:var(--fd-crf-mut)}317.fd-crf-legal .fd-crf-a{color:var(--fd-crf-mut)}318@media (max-width:900px){319 .fd-crf-book{grid-template-columns:1fr;align-items:start}320 .fd-crf-info{grid-template-columns:1fr 1fr}321 .fd-crf-fig{grid-column:1/-1;justify-self:start;max-width:none;display:grid;grid-template-columns:120px 1fr;gap:14px;align-items:center}322 .fd-crf-fig figcaption{margin:0}323}324@media (max-width:560px){325 .fd-crf-info{grid-template-columns:1fr}326 .fd-crf-slots{grid-template-columns:repeat(2,minmax(0,1fr))}327 .fd-crf-big{font-size:3.25rem}328 .fd-crf-done{font-size:2.5rem}329}330@media (prefers-reduced-motion:reduce){331 .fd-crf-in{transform:none;opacity:1}332 .fd-crf *{transition-duration:1ms!important;animation:none!important}333}334@media (prefers-contrast:more){.fd-crf{--fd-crf-mut:#E6D3BC;--fd-crf-line:rgba(244,234,213,.55)}}335`;336 337/* ───────────────────────────── demo ───────────────────────────── */338 339const DOLCI = [340 { n: "Delizia al limone", d: "Sponge, limoncello, a cloud of lemon cream", p: "12" },341 { n: "Torta caprese", d: "Almond and dark chocolate, crème fraîche", p: "11" },342 { n: "Sorbetto del terrazzo", d: "Lemons picked that morning, three metres from your table", p: "8" },343 { n: "Formaggi", d: "Provolone del Monaco, honey from Agerola", p: "16" },344];345 346export function Demo() {347 return (348 <div className="fd-crfd">349 <style>{demoCss}</style>350 <main className="fd-crfd-page">351 <header className="fd-crfd-head">352 <span className="fd-crfd-num">IV</span>353 <h2>Dolci</h2>354 <p>The menu changes with the fishing boats. Last written on Sunday, 2 November.</p>355 </header>356 <ul className="fd-crfd-menu">357 {DOLCI.map((d) => (358 <li key={d.n}>359 <div><b>{d.n}</b><span>{d.d}</span></div>360 <i aria-hidden="true" />361 <em>{d.p}</em>362 </li>363 ))}364 </ul>365 <p className="fd-crfd-foot">Coperto 3. Tell us about allergies, the kitchen is small but listens.</p>366 </main>367 <CurtainRevealFooter />368 </div>369 );370}371 372const demoCss = `373:where(.fd-crfd,.fd-crfd *){box-sizing:border-box;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;color:inherit;list-style:none;font-style:normal}374.fd-crfd{background:#2A1310;font:400 15px/22px ${SANS}}375.fd-crfd-page{padding:clamp(48px,7vw,104px) clamp(16px,4vw,56px) 72px;background:#F4EAD5;color:#2A1310;-webkit-font-smoothing:antialiased}376.fd-crfd-head{display:grid;grid-template-columns:auto 1fr;column-gap:24px;align-items:end;max-width:1080px;margin:0 auto}377.fd-crfd-num{grid-row:span 2;font:italic 400 20px/1 ${SERIF};color:#A4492C;align-self:start;padding-top:18px}378.fd-crfd-head h2{font:400 clamp(3.5rem,8vw,7rem)/.9 ${SERIF};letter-spacing:-.035em}379.fd-crfd-head p{margin-top:16px;max-width:420px;color:#6E5446}380.fd-crfd-menu{max-width:1080px;margin:40px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:0 64px;border-top:1px solid rgba(42,19,16,.2)}381.fd-crfd-menu li{display:grid;grid-template-columns:auto 1fr auto;gap:12px;align-items:baseline;padding:20px 0;border-bottom:1px solid rgba(42,19,16,.12)}382.fd-crfd-menu b{display:block;font:400 22px/28px ${SERIF};letter-spacing:-.01em}383.fd-crfd-menu span{display:block;margin-top:2px;font-size:14px;line-height:20px;color:#6E5446}384.fd-crfd-menu i{display:block;height:1px;border-bottom:1px dotted rgba(42,19,16,.35);align-self:center;margin-top:8px}385.fd-crfd-menu em{font:400 22px/28px ${SERIF};font-variant-numeric:tabular-nums}386.fd-crfd-foot{max-width:1080px;margin:24px auto 0;font:italic 400 15px/22px ${SERIF};color:#6E5446}387@media (max-width:760px){.fd-crfd-menu{grid-template-columns:1fr}.fd-crfd-menu li{grid-template-columns:1fr auto}.fd-crfd-menu i{display:none}}388`;More Footers. Same file contract.
Questions about Curtain Reveal Footer
Run npx shadcn@latest add https://freedesignmd.com/r/curtain-reveal-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.
Curtain reveal footer fixed beneath the page and uncovered as content lifts away, with tables, hours, address and a Book call. 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 (clip-path: inset). Falls back to a static footer when it is taller than the viewport.
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.


