Booking Nav
Hotel booking navbar over a full-bleed photo: dismissible announcement, hide on scroll down and a Dates, Guests, Reserve pill.
- Navbars
- Travel & hospitality
- React only · 0 deps
- 1 file
Browser support · All evergreen browsers. The pill uses backdrop-filter where supported and a solid tint elsewhere.
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/booking-nav.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/navbars/booking-nav.tsx
1/**2 * BookingNav, a boutique hotel header. A dismissible announcement bar sits3 * above a transparent nav laid over a full-bleed photograph. Once the page4 * scrolls the nav turns solid, hides while you scroll down and comes back5 * the moment you scroll up. On the right a compact "Dates · Guests ·6 * Reserve" pill opens a range calendar (with nightly rates and sold-out7 * nights) and a guest stepper.8 *9 * Props: hotel, place, announcement, links, month (year, monthIndex),10 * rates (day → price), soldOut (days), defaultRange, currency, onReserve.11 * CSS vars: --fd-bkn-cream, --fd-bkn-sea, --fd-bkn-terra.12 *13 * Behaviour: the header is `position: fixed`; put it before a full-bleed14 * hero. It reads window scroll.15 * Browser support: all evergreen browsers (backdrop-filter on the pill16 * degrades to a solid tint).17 * Accessibility: popover triggers have aria-expanded/aria-controls; the18 * calendar is a grid of buttons with arrow-key movement, aria-pressed on19 * chosen nights and labels such as "Saturday 12 December, €340"; Escape20 * closes and returns focus. The mobile menu is a modal dialog with a focus21 * trap. The announcement close button moves focus to the hotel name.22 * Reduced motion: no slide on hide/show, popovers appear instantly.23 */24import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";25import type { KeyboardEvent as ReactKeyboardEvent } from "react";26 27export interface BookingNavProps {28 hotel?: string;29 place?: string;30 announcement?: string;31 links?: { label: string; href: string }[];32 month?: { year: number; index: number };33 rates?: (day: number) => number;34 soldOut?: number[];35 defaultRange?: [number, number];36 currency?: string;37 onReserve?: (r: { from: number; to: number; adults: number; children: number }) => void;38 className?: string;39}40 41const LINKS = [42 { label: "Rooms", href: "#rooms" },43 { label: "Terrace", href: "#terrace" },44 { label: "Kitchen", href: "#kitchen" },45 { label: "Journal", href: "#journal" },46];47const SOLD = [24, 25, 31];48const MONTH = { year: 2026, index: 11 };49const RANGE: [number, number] = [12, 15];50const rate = (d: number) => (d >= 18 ? 420 : [5, 6, 12, 13].includes(d) ? 380 : 340);51const WD = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];52 53export function BookingNav({54 hotel = "Sirena",55 place = "Positano · since 1952",56 announcement = "Open all winter: from €340 a night until 15 March, breakfast on the terrace included.",57 links = LINKS,58 month = MONTH,59 rates = rate,60 soldOut = SOLD,61 defaultRange = RANGE,62 currency = "€",63 onReserve,64 className,65}: BookingNavProps) {66 const uid = useId();67 const root = useRef<HTMLElement>(null);68 const ann = useRef<HTMLDivElement>(null);69 const brand = useRef<HTMLAnchorElement>(null);70 const datesBtn = useRef<HTMLButtonElement>(null);71 const guestsBtn = useRef<HTMLButtonElement>(null);72 const burger = useRef<HTMLButtonElement>(null);73 const sheet = useRef<HTMLDivElement>(null);74 const pop = useRef<HTMLDivElement>(null);75 const [annOpen, setAnnOpen] = useState(true);76 const [annH, setAnnH] = useState(0);77 const [solid, setSolid] = useState(false);78 const [hidden, setHidden] = useState(false);79 const [panel, setPanel] = useState<null | "dates" | "guests">(null);80 const [menu, setMenu] = useState(false);81 const [range, setRange] = useState<[number, number | null]>(defaultRange);82 const [adults, setAdults] = useState(2);83 const [kids, setKids] = useState(0);84 const [focusDay, setFocusDay] = useState(defaultRange[0]);85 const [sent, setSent] = useState(false);86 87 useLayoutEffect(() => {88 const el = ann.current;89 if (!el) return;90 const set = () => setAnnH(annOpen ? el.offsetHeight : 0);91 set();92 const ro = new ResizeObserver(set);93 ro.observe(el);94 return () => ro.disconnect();95 }, [annOpen]);96 97 // Solid after the hero top, hide on scroll down, show on scroll up.98 useEffect(() => {99 let last = window.scrollY, raf = 0;100 const on = () => {101 if (raf) return;102 raf = requestAnimationFrame(() => {103 raf = 0;104 const y = window.scrollY;105 setSolid(y > 24);106 if (y < 120) setHidden(false);107 else if (y > last + 6) setHidden(true);108 else if (y < last - 6) setHidden(false);109 last = y;110 });111 };112 on();113 window.addEventListener("scroll", on, { passive: true });114 return () => {115 window.removeEventListener("scroll", on);116 cancelAnimationFrame(raf);117 };118 }, []);119 120 // Popover: Escape, outside click, focus.121 useEffect(() => {122 if (!panel) return;123 const opener = panel === "dates" ? datesBtn.current : guestsBtn.current;124 window.setTimeout(() => {125 const p = pop.current;126 (p?.querySelector<HTMLElement>(`[data-day="${focusDay}"]`) ?? p?.querySelector<HTMLElement>("button"))?.focus();127 }, 30);128 const onKey = (e: KeyboardEvent) => {129 if (e.key === "Escape") {130 setPanel(null);131 opener?.focus();132 }133 };134 const onDown = (e: PointerEvent) => {135 const t = e.target as Node;136 if (!pop.current?.contains(t) && !datesBtn.current?.contains(t) && !guestsBtn.current?.contains(t)) setPanel(null);137 };138 document.addEventListener("keydown", onKey);139 document.addEventListener("pointerdown", onDown);140 return () => {141 document.removeEventListener("keydown", onKey);142 document.removeEventListener("pointerdown", onDown);143 };144 }, [panel]); // eslint-disable-line react-hooks/exhaustive-deps145 146 // Mobile menu.147 useEffect(() => {148 if (!menu) return;149 const s = sheet.current;150 window.setTimeout(() => s?.querySelector<HTMLElement>("a,button")?.focus(), 40);151 const onKey = (e: KeyboardEvent) => {152 if (e.key === "Escape") {153 setMenu(false);154 burger.current?.focus();155 }156 if (e.key === "Tab" && s) {157 const f = Array.from(s.querySelectorAll<HTMLElement>("a,button")).filter((x) => x.offsetParent !== null);158 const i = f.indexOf(document.activeElement as HTMLElement);159 if (e.shiftKey && i <= 0) { e.preventDefault(); f[f.length - 1].focus(); }160 else if (!e.shiftKey && i === f.length - 1) { e.preventDefault(); f[0].focus(); }161 }162 };163 const mq = matchMedia("(min-width: 901px)");164 const onMq = () => mq.matches && setMenu(false);165 document.addEventListener("keydown", onKey);166 mq.addEventListener("change", onMq);167 return () => {168 document.removeEventListener("keydown", onKey);169 mq.removeEventListener("change", onMq);170 };171 }, [menu]);172 173 // Calendar maths (UTC, no "now", so server and client agree).174 const first = new Date(Date.UTC(month.year, month.index, 1));175 const days = new Date(Date.UTC(month.year, month.index + 1, 0)).getUTCDate();176 const lead = (first.getUTCDay() + 6) % 7;177 const monthName = first.toLocaleString("en-GB", { month: "long", timeZone: "UTC" });178 const monShort = first.toLocaleString("en-GB", { month: "short", timeZone: "UTC" });179 const dayName = (d: number) => new Date(Date.UTC(month.year, month.index, d)).toLocaleString("en-GB", { weekday: "long", timeZone: "UTC" });180 const [from, to] = range;181 const nights = to ? to - from : 0;182 const blocked = (a: number, b: number) => soldOut.some((s) => s >= a && s < b);183 const total = to ? Array.from({ length: nights }, (_, i) => rates(from + i)).reduce((x, y) => x + y, 0) : 0;184 185 const pickDay = (d: number) => {186 if (soldOut.includes(d) && (to !== null || d <= from)) return;187 if (to === null && d > from && !blocked(from, d)) setRange([from, d]);188 else setRange([d, null]);189 setFocusDay(d);190 };191 const gridKey = (e: ReactKeyboardEvent) => {192 const m: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };193 if (!(e.key in m)) return;194 e.preventDefault();195 const n = Math.min(days, Math.max(1, focusDay + m[e.key]));196 setFocusDay(n);197 (e.currentTarget as HTMLElement).querySelector<HTMLElement>(`[data-day="${n}"]`)?.focus();198 };199 200 const dateLabel = to ? `${from}–${to} ${monShort}` : `${from} ${monShort} →`;201 const guestLabel = `${adults + kids} ${adults + kids === 1 ? "guest" : "guests"}`;202 const reserve = () => {203 if (!to) {204 setPanel("dates");205 return;206 }207 onReserve?.({ from, to, adults, children: kids });208 setSent(true);209 setPanel(null);210 window.setTimeout(() => setSent(false), 2600);211 };212 213 const calendar = (214 <div className="fd-bkn-cal">215 <div className="fd-bkn-calhead">216 <b>{monthName} {month.year}</b>217 <span>{to ? `${nights} ${nights === 1 ? "night" : "nights"} · ${currency}${total.toLocaleString("en-GB")}` : "Choose your last night"}</span>218 </div>219 <div className="fd-bkn-grid" role="grid" aria-label={`${monthName} ${month.year}`} onKeyDown={gridKey}>220 <div role="row" className="fd-bkn-wk">{WD.map((w) => <span key={w} role="columnheader">{w}</span>)}</div>221 {Array.from({ length: Math.ceil((lead + days) / 7) }, (_, r) => (222 <div role="row" key={r} className="fd-bkn-wk">223 {Array.from({ length: 7 }, (_, c) => {224 const d = r * 7 + c - lead + 1;225 if (d < 1 || d > days) return <span key={c} role="gridcell" />;226 const sold = soldOut.includes(d);227 const isEnd = d === from || d === to;228 const inRange = to !== null && d > from && d < to;229 return (230 <span key={c} role="gridcell">231 <button232 type="button"233 data-day={d}234 tabIndex={d === focusDay ? 0 : -1}235 className="fd-bkn-day"236 data-end={isEnd ? "" : undefined}237 data-in={inRange ? "" : undefined}238 data-start={d === from ? "" : undefined}239 data-stop={d === to ? "" : undefined}240 aria-pressed={isEnd || inRange}241 aria-disabled={sold || undefined}242 aria-label={`${dayName(d)} ${d} ${monthName}, ${sold ? "sold out" : `${currency}${rates(d)}`}`}243 onClick={() => pickDay(d)}244 >245 <b>{d}</b>246 <small>{sold ? "full" : rates(d)}</small>247 </button>248 </span>249 );250 })}251 </div>252 ))}253 </div>254 <p className="fd-bkn-calnote">Prices per night for two, breakfast included. Check-in from 15:00.</p>255 </div>256 );257 const stepper = (label: string, sub: string, v: number, set: (n: number) => void, min: number, max: number) => (258 <div className="fd-bkn-step">259 <span><b>{label}</b><small>{sub}</small></span>260 <button type="button" aria-label={`Fewer ${label.toLowerCase()}`} disabled={v <= min} onClick={() => set(v - 1)}>−</button>261 <output aria-live="polite" aria-label={label}>{v}</output>262 <button type="button" aria-label={`More ${label.toLowerCase()}`} disabled={v >= max} onClick={() => set(v + 1)}>+</button>263 </div>264 );265 const guests = (266 <div className="fd-bkn-guests">267 {stepper("Adults", "13 and over", adults, setAdults, 1, 4)}268 {stepper("Children", "Cots free, under 3", kids, setKids, 0, 3)}269 <p className="fd-bkn-calnote">Our largest room, the Torre suite, sleeps four.</p>270 </div>271 );272 273 // No transform while the menu is open: a transformed ancestor would trap the fixed sheet.274 const shift = menu ? null : hidden && !panel ? "-100%" : solid ? `-${annH}px` : null;275 276 return (277 <header278 ref={root}279 className={`fd-bkn${className ? " " + className : ""}`}280 data-solid={solid || menu ? "" : undefined}281 style={shift ? { transform: `translateY(${shift})` } : undefined}282 >283 <style>{css}</style>284 <div className="fd-bkn-annwrap" data-open={annOpen ? "" : undefined}>285 <div ref={ann} className="fd-bkn-ann" inert={!annOpen ? true : undefined}>286 <p>{announcement} <a href="#winter">See winter dates</a></p>287 <button type="button" className="fd-bkn-annx" aria-label="Dismiss announcement" onClick={() => { setAnnOpen(false); brand.current?.focus(); }}>288 <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /></svg>289 </button>290 </div>291 </div>292 <div className="fd-bkn-bar">293 <nav className="fd-bkn-links" aria-label="Hotel">294 {links.map((l) => <a key={l.href} href={l.href}>{l.label}</a>)}295 </nav>296 <a ref={brand} href="#top" className="fd-bkn-brand" aria-label={`${hotel}, ${place}`}>297 <span className="fd-bkn-word">{hotel}</span>298 <span className="fd-bkn-place">{place}</span>299 </a>300 <div className="fd-bkn-right">301 <div className="fd-bkn-pill">302 <button ref={datesBtn} type="button" className="fd-bkn-seg" aria-expanded={panel === "dates"} aria-controls={`${uid}-pop`} onClick={() => setPanel((p) => (p === "dates" ? null : "dates"))}>303 <small>Dates</small><b>{dateLabel}</b>304 </button>305 <span className="fd-bkn-dot" aria-hidden="true" />306 <button ref={guestsBtn} type="button" className="fd-bkn-seg" aria-expanded={panel === "guests"} aria-controls={`${uid}-pop`} onClick={() => setPanel((p) => (p === "guests" ? null : "guests"))}>307 <small>Guests</small><b>{guestLabel}</b>308 </button>309 <button type="button" className="fd-bkn-reserve" onClick={reserve}>310 <span>{sent ? "Request sent" : "Reserve"}</span>311 </button>312 </div>313 <button type="button" className="fd-bkn-mreserve" onClick={() => { setMenu(true); }}>Reserve</button>314 <button ref={burger} type="button" className="fd-bkn-burger" aria-label="Menu" aria-expanded={menu} aria-controls={`${uid}-sheet`} onClick={() => setMenu((m) => !m)}>315 <span /><span />316 </button>317 {panel && (318 <div ref={pop} id={`${uid}-pop`} className="fd-bkn-pop" role="dialog" aria-label={panel === "dates" ? "Choose dates" : "Guests"} data-kind={panel}>319 {panel === "dates" ? calendar : guests}320 <div className="fd-bkn-popfoot">321 <button type="button" className="fd-bkn-text" onClick={() => (panel === "dates" ? setRange([from, null]) : (setAdults(2), setKids(0)))}>Clear</button>322 <button type="button" className="fd-bkn-reserve fd-bkn-reserve-lg" onClick={() => (panel === "dates" && to ? setPanel("guests") : reserve())}>323 {panel === "dates" ? (to ? "Next: guests" : "Pick a last night") : `Reserve · ${currency}${total.toLocaleString("en-GB")}`}324 </button>325 </div>326 </div>327 )}328 </div>329 </div>330 {sent && <p className="fd-bkn-toast" role="status">Request sent for {dateLabel}, {guestLabel}. Chiara will reply within the hour.</p>}331 332 <div ref={sheet} id={`${uid}-sheet`} className="fd-bkn-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden={!menu} style={{ top: annH + 64 }}>333 <nav aria-label="Hotel">334 {links.map((l, i) => <a key={l.href} href={l.href} style={{ animationDelay: `${i * 50}ms` }} onClick={() => setMenu(false)}>{l.label}</a>)}335 </nav>336 <div className="fd-bkn-mbook">337 <p className="fd-bkn-mh">Stay with us</p>338 {calendar}339 {guests}340 <button type="button" className="fd-bkn-reserve fd-bkn-reserve-lg fd-bkn-mfull" disabled={!to} onClick={() => { if (to) { reserve(); setMenu(false); } }}>341 {to ? `Reserve ${dateLabel} · ${currency}${total.toLocaleString("en-GB")}` : "Pick a last night"}342 </button>343 </div>344 </div>345 </header>346 );347}348 349const serif = `"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif`;350const sans = `ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif`;351 352const css = `353.fd-bkn {354 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);355 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);356 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);357 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;358 --fd-bkn-cream: #f6efe3;359 --fd-bkn-sea: #0f2a3a;360 --fd-bkn-terra: #b4532f;361 --fd-bkn-line: #e2d6c3;362 --fd-bkn-fg: #fff;363 position: fixed; left: 0; right: 0; top: 0; z-index: 60; display: block; margin: 0; padding: 0;364 font: 400 14px/1.4 ${sans};365 color: var(--fd-bkn-fg); background: transparent;366 transition: transform var(--fd-dur-3) var(--fd-ease-out);367}368.fd-bkn[data-solid] { --fd-bkn-fg: var(--fd-bkn-sea); }369.fd-bkn, .fd-bkn *, .fd-bkn *::before, .fd-bkn *::after { box-sizing: border-box; }370.fd-bkn a:focus-visible, .fd-bkn button:focus-visible { outline: 2px solid color-mix(in srgb, var(--fd-bkn-terra) 75%, transparent); outline-offset: 2px; }371 372.fd-bkn-annwrap { display: grid; grid-template-rows: 0fr; margin: 0; padding: 0; transition: grid-template-rows var(--fd-dur-3) var(--fd-ease-in-out); }373.fd-bkn-annwrap[data-open] { grid-template-rows: 1fr; }374.fd-bkn-ann { min-height: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; gap: 12px; margin: 0; padding: 0 48px; background: var(--fd-bkn-sea); color: #f6efe3; position: relative; }375.fd-bkn-ann p { margin: 0; padding: 10px 0; font: 400 0.8125rem/1.35 ${sans}; color: #f6efe3; text-align: center; }376.fd-bkn-ann a { margin: 0 0 0 6px; font: italic 400 0.875rem/1 ${serif}; color: #f2c9a8; text-decoration: underline; text-underline-offset: 3px; }377.fd-bkn-annx { position: absolute; right: 10px; top: 50%; display: grid; place-items: center; width: 30px; height: 30px; margin: -15px 0 0; padding: 0; border: 0; border-radius: 9999px; background: none; color: #f6efe3; cursor: pointer; appearance: none; -webkit-appearance: none; }378.fd-bkn-annx:hover { background: rgba(246,239,227,0.12); }379.fd-bkn-annx svg { display: block; }380 381.fd-bkn-bar { position: relative; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 84px; margin: 0; padding: 0 28px; color: var(--fd-bkn-fg); transition: background var(--fd-dur-3) linear, color var(--fd-dur-3) linear, box-shadow var(--fd-dur-3) linear; }382.fd-bkn:not([data-solid]) .fd-bkn-bar { background: linear-gradient(rgba(10,20,28,0.42), rgba(10,20,28,0)); text-shadow: 0 1px 12px rgba(0,0,0,0.25); }383.fd-bkn[data-solid] .fd-bkn-bar { background: var(--fd-bkn-cream); box-shadow: 0 1px 0 var(--fd-bkn-line); height: 72px; }384.fd-bkn-links { display: flex; gap: 26px; margin: 0; padding: 0; }385.fd-bkn-links a { position: relative; font: 500 0.8125rem/1 ${sans}; letter-spacing: 0.06em; text-transform: uppercase; color: inherit; text-decoration: none; padding: 6px 0; }386.fd-bkn-links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform var(--fd-dur-2) var(--fd-ease-out); }387.fd-bkn-links a:hover::after { transform: scaleX(1); transform-origin: left; }388.fd-bkn-brand { display: flex; flex-direction: column; align-items: center; gap: 2px; margin: 0; padding: 2px 8px; color: inherit; text-decoration: none; border-radius: 4px; }389.fd-bkn-word { font: italic 400 2.25rem/0.95 ${serif}; letter-spacing: -0.02em; color: inherit; transition: font-size var(--fd-dur-3) var(--fd-ease-out); }390.fd-bkn[data-solid] .fd-bkn-word { font-size: 1.875rem; }391.fd-bkn-place { font: 500 0.625rem/1 ${sans}; letter-spacing: 0.22em; text-transform: uppercase; opacity: 0.85; }392.fd-bkn-right { position: relative; display: flex; justify-content: flex-end; align-items: center; gap: 10px; }393 394.fd-bkn-pill { display: flex; align-items: center; margin: 0; padding: 4px 4px 4px 6px; border: 1px solid rgba(255,255,255,0.35); border-radius: 9999px; background: rgba(15,42,58,0.22); -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2); text-shadow: none; transition: background var(--fd-dur-3) linear, border-color var(--fd-dur-3) linear; }395.fd-bkn[data-solid] .fd-bkn-pill { background: #fff; border-color: var(--fd-bkn-line); -webkit-backdrop-filter: none; backdrop-filter: none; }396.fd-bkn-seg { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; margin: 0; padding: 6px 12px; border: 0; border-radius: 9999px; background: none; color: inherit; cursor: pointer; appearance: none; -webkit-appearance: none; text-align: left; transition: background var(--fd-dur-1) linear; }397.fd-bkn-seg:hover, .fd-bkn-seg[aria-expanded="true"] { background: rgba(255,255,255,0.16); }398.fd-bkn[data-solid] .fd-bkn-seg:hover, .fd-bkn[data-solid] .fd-bkn-seg[aria-expanded="true"] { background: #f3ebde; }399.fd-bkn-seg small { font: 500 0.625rem/1 ${sans}; letter-spacing: 0.14em; text-transform: uppercase; color: inherit; opacity: 0.75; }400.fd-bkn-seg b { font: 400 0.9375rem/1 ${serif}; color: inherit; white-space: nowrap; font-variant-numeric: tabular-nums; }401.fd-bkn-dot { display: block; width: 3px; height: 3px; border-radius: 9999px; background: currentColor; opacity: 0.5; }402.fd-bkn-reserve { display: inline-flex; align-items: center; justify-content: center; height: 40px; margin: 0 0 0 6px; padding: 0 18px; border: 0; border-radius: 9999px; background: var(--fd-bkn-terra); color: #fff; font: 600 0.8125rem/1 ${sans}; letter-spacing: 0.04em; cursor: pointer; appearance: none; -webkit-appearance: none; white-space: nowrap; transition: transform var(--fd-dur-1) var(--fd-ease-out), background var(--fd-dur-1) linear; }403.fd-bkn-reserve:hover { background: #9e4626; }404.fd-bkn-reserve:active { transform: scale(0.96); }405.fd-bkn-reserve-lg { height: 44px; margin: 0; padding: 0 20px; }406 407.fd-bkn-pop { position: absolute; right: 0; top: calc(100% + 10px); z-index: 3; width: 360px; margin: 0; padding: 18px; border: 1px solid var(--fd-bkn-line); border-radius: 22px; background: var(--fd-bkn-cream); color: var(--fd-bkn-sea); text-shadow: none; box-shadow: 0 30px 60px -12px rgba(15,42,58,0.35); animation: fd-bkn-pop var(--fd-dur-3) var(--fd-ease-out); transform-origin: top right; }408.fd-bkn-pop[data-kind="guests"] { width: 320px; }409@keyframes fd-bkn-pop { from { opacity: 0; transform: translateY(-6px) scale(0.98); } }410.fd-bkn-cal, .fd-bkn-guests { margin: 0; padding: 0; color: var(--fd-bkn-sea); }411.fd-bkn-calhead { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin: 0 0 12px; }412.fd-bkn-calhead b { font: italic 400 1.25rem/1 ${serif}; color: var(--fd-bkn-sea); }413.fd-bkn-calhead span { font: 500 0.75rem/1 ${sans}; color: var(--fd-bkn-terra); }414.fd-bkn-grid { display: block; margin: 0; padding: 0; }415.fd-bkn-wk { display: grid; grid-template-columns: repeat(7, 1fr); }416.fd-bkn-wk > span { display: block; margin: 0; padding: 0; }417.fd-bkn-wk:first-child span { padding: 0 0 6px; font: 500 0.6875rem/1 ${sans}; letter-spacing: 0.06em; color: #7b8790; text-align: center; }418.fd-bkn-day { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; width: 100%; height: 44px; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; color: var(--fd-bkn-sea); cursor: pointer; appearance: none; -webkit-appearance: none; }419.fd-bkn-day b { font: 400 0.9375rem/1 ${serif}; font-variant-numeric: tabular-nums; }420.fd-bkn-day small { font: 400 0.5625rem/1 ${sans}; color: #7b8790; }421.fd-bkn-day:hover b { text-decoration: underline; text-underline-offset: 3px; }422.fd-bkn-day[data-in] { background: #ecdcc6; }423.fd-bkn-day[data-start] { background: linear-gradient(90deg, transparent 50%, #ecdcc6 50%); }424.fd-bkn-day[data-stop] { background: linear-gradient(90deg, #ecdcc6 50%, transparent 50%); }425.fd-bkn-day[data-start]:not([data-stop]):last-child { background: none; }426.fd-bkn-day[data-end]::before { content: ""; position: absolute; left: 50%; top: 50%; width: 42px; height: 42px; margin: -21px 0 0 -21px; border-radius: 9999px; background: var(--fd-bkn-sea); z-index: 0; animation: fd-bkn-dot var(--fd-dur-2) var(--fd-spring); }427@keyframes fd-bkn-dot { from { transform: scale(0.6); } }428.fd-bkn-day[data-end] b, .fd-bkn-day[data-end] small { position: relative; z-index: 1; color: #f6efe3; }429.fd-bkn-day[aria-disabled="true"] { cursor: not-allowed; }430.fd-bkn-day[aria-disabled="true"] b { color: #a9b2b8; text-decoration: line-through; }431.fd-bkn-calnote { margin: 12px 0 0; font: italic 400 0.8125rem/1.35 ${serif}; color: #5b6a74; }432.fd-bkn-step { display: grid; grid-template-columns: 1fr 36px 32px 36px; align-items: center; gap: 4px; padding: 12px 0; border-bottom: 1px solid var(--fd-bkn-line); }433.fd-bkn-step > span { display: flex; flex-direction: column; gap: 3px; }434.fd-bkn-step b { font: 400 1.0625rem/1 ${serif}; color: var(--fd-bkn-sea); }435.fd-bkn-step small { font: 400 0.75rem/1 ${sans}; color: #7b8790; }436.fd-bkn-step button { display: grid; place-items: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 1px solid #cdbfa9; border-radius: 9999px; background: none; color: var(--fd-bkn-sea); font: 400 1.125rem/1 ${sans}; cursor: pointer; appearance: none; -webkit-appearance: none; }437.fd-bkn-step button:disabled { opacity: 0.35; cursor: default; }438.fd-bkn-step output { font: 400 1.125rem/1 ${serif}; text-align: center; color: var(--fd-bkn-sea); }439.fd-bkn-popfoot { display: flex; justify-content: space-between; align-items: center; margin: 14px 0 0; padding: 14px 0 0; border-top: 1px solid var(--fd-bkn-line); }440.fd-bkn-text { margin: 0; padding: 8px 4px; border: 0; background: none; font: 500 0.8125rem/1 ${sans}; color: var(--fd-bkn-sea); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; appearance: none; -webkit-appearance: none; }441.fd-bkn-toast { position: fixed; left: 50%; bottom: 24px; z-index: 70; max-width: calc(100vw - 32px); margin: 0; padding: 12px 18px; border-radius: 14px; background: var(--fd-bkn-sea); color: #f6efe3; font: 400 0.9375rem/1.35 ${serif}; transform: translateX(-50%); box-shadow: 0 16px 40px rgba(15,42,58,0.35); animation: fd-bkn-toast var(--fd-dur-3) var(--fd-spring); }442@keyframes fd-bkn-toast { from { opacity: 0; transform: translate(-50%, 12px); } }443 444.fd-bkn-burger, .fd-bkn-mreserve { display: none; }445.fd-bkn-burger { position: relative; width: 42px; height: 42px; margin: 0 -8px 0 0; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; appearance: none; -webkit-appearance: none; }446.fd-bkn-burger span { position: absolute; left: 11px; right: 11px; height: 1.5px; background: currentColor; transition: transform var(--fd-dur-3) var(--fd-spring); }447.fd-bkn-burger span:first-child { top: 17px; }448.fd-bkn-burger span:last-child { top: 24px; }449.fd-bkn-burger[aria-expanded="true"] span:first-child { transform: translateY(3.5px) rotate(45deg); }450.fd-bkn-burger[aria-expanded="true"] span:last-child { transform: translateY(-3.5px) rotate(-45deg); }451.fd-bkn-mreserve { height: 36px; margin: 0; padding: 0 14px; border: 0; border-radius: 9999px; background: var(--fd-bkn-terra); color: #fff; font: 600 0.75rem/1 ${sans}; letter-spacing: 0.04em; cursor: pointer; appearance: none; -webkit-appearance: none; }452.fd-bkn-sheet { position: fixed; left: 0; right: 0; top: 64px; bottom: 0; z-index: 2; overflow-y: auto; margin: 0; padding: 12px 20px 32px; background: var(--fd-bkn-cream); color: var(--fd-bkn-sea); text-shadow: none; animation: fd-bkn-sheet var(--fd-dur-3) var(--fd-ease-out); }453.fd-bkn-sheet[hidden] { display: none; }454@keyframes fd-bkn-sheet { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }455.fd-bkn-sheet nav { display: block; margin: 0 0 20px; padding: 0; }456.fd-bkn-sheet nav a { display: block; margin: 0; padding: 12px 0; border-bottom: 1px solid var(--fd-bkn-line); font: italic 400 2.25rem/1 ${serif}; letter-spacing: -0.02em; color: var(--fd-bkn-sea); text-decoration: none; animation: fd-bkn-rise var(--fd-dur-3) var(--fd-ease-out) both; }457@keyframes fd-bkn-rise { from { opacity: 0; transform: translateY(10px); } }458.fd-bkn-mbook { margin: 0; padding: 18px; border: 1px solid var(--fd-bkn-line); border-radius: 22px; background: #fbf7f0; }459.fd-bkn-mh { margin: 0 0 14px; font: 500 0.6875rem/1 ${sans}; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fd-bkn-terra); }460.fd-bkn-mbook .fd-bkn-guests { margin: 8px 0 16px; }461.fd-bkn-mfull { width: 100%; }462 463@media (max-width: 1100px) { .fd-bkn-links { gap: 18px; } .fd-bkn-links a:nth-child(n+4) { display: none; } }464@media (max-width: 900px) {465 .fd-bkn-bar, .fd-bkn[data-solid] .fd-bkn-bar { grid-template-columns: 1fr auto; height: 64px; padding: 0 18px; }466 .fd-bkn-links, .fd-bkn-pill { display: none; }467 .fd-bkn-brand { align-items: flex-start; padding-left: 0; }468 .fd-bkn-word, .fd-bkn[data-solid] .fd-bkn-word { font-size: 1.75rem; }469 .fd-bkn-burger, .fd-bkn-mreserve { display: block; }470 .fd-bkn-ann { padding: 0 44px 0 16px; justify-content: flex-start; }471 .fd-bkn-ann p { text-align: left; font-size: 0.75rem; }472}473@media (prefers-reduced-motion: reduce) {474 .fd-bkn, .fd-bkn-annwrap, .fd-bkn-bar, .fd-bkn-word, .fd-bkn-burger span { transition: none; }475 .fd-bkn-pop, .fd-bkn-sheet, .fd-bkn-sheet nav a, .fd-bkn-toast, .fd-bkn-day[data-end]::before { animation: none; }476}477@media (prefers-reduced-transparency: reduce) { .fd-bkn-pill { -webkit-backdrop-filter: none; backdrop-filter: none; background: rgba(15,42,58,0.75); } }478`;479 480/* ── Demo ─────────────────────────────────────────────────────────────── */481 482export function Demo({ src = "/img/halcyon/positano-sunset.webp", roomSrc = "/img/halcyon/suite-900.webp" }: { src?: string; roomSrc?: string }) {483 return (484 <div className="fd-bkn-demo">485 <style>{demoCss}</style>486 <BookingNav />487 <section className="fd-bkn-demo-hero">488 <img src={src} alt="Positano at sunset, seen from the hotel terrace" width={1024} height={1024} />489 <div className="fd-bkn-demo-copy">490 <p>Hotel Sirena · Via Pasitea 118 · 23 rooms</p>491 <h1>A small hotel above the beach, <i>open all winter.</i></h1>492 </div>493 </section>494 <section className="fd-bkn-demo-rooms" id="rooms">495 <div>496 <p className="fd-bkn-demo-k">Rooms</p>497 <h2>Twenty-three rooms, nineteen with the sea in the window.</h2>498 <p>Terracotta floors from Vietri, linen from a mill in Biella, and shutters that still close with a hook. The Torre suite sits in the 16th century watchtower and sleeps four.</p>499 <ul>500 <li><b>Camera Mare</b><span>24 m² · from €340</span></li>501 <li><b>Terrazza</b><span>38 m² · from €480</span></li>502 <li><b>Suite Torre</b><span>62 m² · from €890</span></li>503 </ul>504 </div>505 <img src={roomSrc} alt="Suite with linen bed and a sea view" width={900} height={900} loading="lazy" />506 </section>507 </div>508 );509}510 511const demoCss = `512.fd-bkn-demo { margin: 0; padding: 0; background: #f6efe3; color: #0f2a3a; font: 400 15px/1.55 ${sans}; }513.fd-bkn-demo *, .fd-bkn-demo *::before, .fd-bkn-demo *::after { box-sizing: border-box; }514.fd-bkn-demo-hero { position: relative; height: 100vh; min-height: 620px; margin: 0; overflow: hidden; background: #2a2622; }515.fd-bkn-demo-hero img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 60%; }516.fd-bkn-demo-hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 45%, rgba(10,20,28,0.55)); }517.fd-bkn-demo-copy { position: absolute; left: 28px; right: 28px; bottom: 44px; z-index: 1; color: #fff; }518.fd-bkn-demo-copy p { margin: 0 0 14px; font: 500 0.75rem/1 ${sans}; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(255,255,255,0.86); }519.fd-bkn-demo-copy h1 { max-width: 15ch; margin: 0; font: 400 clamp(2.5rem, 6.4vw, 5.75rem)/0.98 ${serif}; letter-spacing: -0.03em; color: #fff; }520.fd-bkn-demo-rooms { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: center; max-width: 1240px; margin: 0 auto; padding: 96px 28px 140px; }521.fd-bkn-demo-k { margin: 0 0 14px; font: 500 0.6875rem/1 ${sans}; letter-spacing: 0.16em; text-transform: uppercase; color: #b4532f; }522.fd-bkn-demo-rooms h2 { margin: 0 0 18px; font: 400 2.5rem/1.05 ${serif}; letter-spacing: -0.02em; }523.fd-bkn-demo-rooms p { margin: 0 0 24px; max-width: 46ch; font: 400 1.0625rem/1.6 ${serif}; color: #3b4f5c; }524.fd-bkn-demo-rooms ul { margin: 0; padding: 0; list-style: none; border-top: 1px solid #e2d6c3; }525.fd-bkn-demo-rooms li { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid #e2d6c3; }526.fd-bkn-demo-rooms li b { font: italic 400 1.25rem/1 ${serif}; }527.fd-bkn-demo-rooms li span { font: 400 0.875rem/1 ${sans}; color: #5b6a74; }528.fd-bkn-demo-rooms img { display: block; width: 100%; aspect-ratio: 4 / 5; height: auto; object-fit: cover; border-radius: 200px 200px 0 0; }529@media (max-width: 900px) {530 .fd-bkn-demo-copy { left: 18px; right: 18px; bottom: 32px; }531 .fd-bkn-demo-rooms { grid-template-columns: 1fr; gap: 32px; padding: 56px 18px 80px; }532 .fd-bkn-demo-rooms h2 { font-size: 1.875rem; }533}534`;More Navbars. Same file contract.
Questions about Booking Nav
Run npx shadcn@latest add https://freedesignmd.com/r/booking-nav.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.
Hotel booking navbar over a full-bleed photo: dismissible announcement, hide on scroll down and a Dates, Guests, Reserve pill. It is a free, MIT-licensed React navbars 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. The pill uses backdrop-filter where supported and a solid tint elsewhere.
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.


