Skip to content

Floating Capsule Nav

Sticky navbar that springs from a full-width bar into a floating capsule on scroll, with a live status light and version chip.

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

Browser support · All evergreen browsers (clip-path inset with round, color-mix: Chrome 111+, Safari 16.2+, Firefox 113+).

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/floating-capsule-nav.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/navbars/floating-capsule-nav.tsx
1/**
2 * FloatingCapsuleNav, a full-width developer-tools bar that, once the page
3 * scrolls, contracts into a floating capsule centred at the top. The shape is
4 * driven by a small spring integrator (k 380, c 30) so it settles with one
5 * measured overshoot instead of a CSS ease. Secondary items (version chip,
6 * status label, GitHub count, sign in) fade out as the capsule forms; the
7 * status light moves onto the logo mark so it is never lost.
8 *
9 * Props: brand, version, status ({ label, href, state }), links, stars,
10 * signIn, cta, threshold (px of scroll before contracting), capsuleWidth.
11 * CSS vars on the root: --fd-fcn-bg, --fd-fcn-ink, --fd-fcn-accent.
12 *
13 * Behaviour: the header is `position: sticky; top: 0`, so place it as the
14 * first child of a tall page wrapper. It reads window scroll.
15 * Browser support: all evergreen browsers (clip-path inset with round).
16 * Accessibility: <header> + <nav aria-label="Primary">; the mobile menu button
17 * has aria-expanded/aria-controls, Escape closes and returns focus, Tab is
18 * kept inside the open menu, hidden items are removed from the tab order.
19 * Reduced motion: the capsule snaps between states, no status pulse.
20 */
21import { useEffect, useId, useRef, useState } from "react";
22
23export interface FcnLink { label: string; href: string; badge?: string }
24export interface FloatingCapsuleNavProps {
25 brand?: string;
26 version?: string;
27 status?: { label: string; href: string; state?: "ok" | "degraded" | "down" };
28 links?: FcnLink[];
29 stars?: string;
30 signIn?: { label: string; href: string };
31 cta?: { label: string; href: string };
32 threshold?: number;
33 capsuleWidth?: number;
34 className?: string;
35}
36
37const LINKS: FcnLink[] = [
38 { label: "Docs", href: "#docs" },
39 { label: "Remote cache", href: "#cache" },
40 { label: "Pricing", href: "#pricing" },
41 { label: "Changelog", href: "#changelog", badge: "New" },
42 { label: "Customers", href: "#customers" },
43];
44
45export function FloatingCapsuleNav({
46 brand = "kiln",
47 version = "v4.2.0",
48 status = { label: "All systems normal", href: "#status", state: "ok" },
49 links = LINKS,
50 stars = "18.4k",
51 signIn = { label: "Sign in", href: "#signin" },
52 cta = { label: "Start building", href: "#start" },
53 threshold = 24,
54 capsuleWidth = 860,
55 className,
56}: FloatingCapsuleNavProps) {
57 const uid = useId();
58 const sheetId = `${uid}-menu`;
59 const root = useRef<HTMLElement>(null);
60 const burger = useRef<HTMLButtonElement>(null);
61 const sheet = useRef<HTMLDivElement>(null);
62 const navRef = useRef<HTMLElement>(null);
63 const hover = useRef<HTMLSpanElement>(null);
64 const [open, setOpen] = useState(false);
65 const [compact, setCompact] = useState(false);
66
67 // Spring-driven capsule.
68 useEffect(() => {
69 const el = root.current;
70 if (!el) return;
71 const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
72 let x = 0, v = 0, target = 0, raf = 0, last = 0;
73 const apply = () => {
74 const W = el.clientWidth;
75 const mobile = W < 760;
76 const margin = mobile ? 12 : 16;
77 const cap = Math.min(W - margin * 2, capsuleWidth);
78 const s = ((W - cap) / 2) * x;
79 const pad = mobile ? 20 : 32;
80 const capPad = mobile ? 10 : 12;
81 el.style.setProperty("--fcn-s", `${s.toFixed(2)}px`);
82 el.style.setProperty("--fcn-t", `${(10 * Math.min(Math.max(x, 0), 1.2)).toFixed(2)}px`);
83 el.style.setProperty("--fcn-r", `${(28 * Math.min(Math.max(x, 0), 1)).toFixed(2)}px`);
84 el.style.setProperty("--fcn-dx", `${(s + (capPad - pad) * x).toFixed(2)}px`);
85 el.style.setProperty("--fcn-p", `${Math.min(Math.max(x, 0), 1).toFixed(3)}`);
86 el.style.setProperty("--fcn-fade", `${Math.max(0, 1 - x * 2.4).toFixed(3)}`);
87 };
88 const step = (now: number) => {
89 const dt = Math.min(0.032, (now - last) / 1000 || 0.016);
90 last = now;
91 const a = -380 * (x - target) - 30 * v;
92 v += a * dt;
93 x += v * dt;
94 apply();
95 if (Math.abs(v) < 0.002 && Math.abs(x - target) < 0.001) {
96 x = target;
97 v = 0;
98 apply();
99 raf = 0;
100 return;
101 }
102 raf = requestAnimationFrame(step);
103 };
104 const kick = () => {
105 if (reduce) {
106 x = target;
107 apply();
108 return;
109 }
110 if (!raf) {
111 last = performance.now();
112 raf = requestAnimationFrame(step);
113 }
114 };
115 const onScroll = () => {
116 const t = window.scrollY > threshold ? 1 : 0;
117 if (t !== target) {
118 target = t;
119 setCompact(t === 1);
120 kick();
121 }
122 };
123 target = window.scrollY > threshold ? 1 : 0;
124 x = target;
125 setCompact(target === 1);
126 apply();
127 const ro = new ResizeObserver(apply);
128 ro.observe(el);
129 window.addEventListener("scroll", onScroll, { passive: true });
130 return () => {
131 window.removeEventListener("scroll", onScroll);
132 ro.disconnect();
133 cancelAnimationFrame(raf);
134 };
135 }, [threshold, capsuleWidth]);
136
137 // Mobile menu: Escape, focus in, focus trap, close when resized to desktop.
138 useEffect(() => {
139 if (!open) return;
140 const first = sheet.current?.querySelector<HTMLElement>("a,button");
141 first?.focus();
142 const onKey = (e: KeyboardEvent) => {
143 if (e.key === "Escape") {
144 setOpen(false);
145 burger.current?.focus();
146 }
147 if (e.key === "Tab" && sheet.current) {
148 const f = [burger.current!, ...Array.from(sheet.current.querySelectorAll<HTMLElement>("a,button"))];
149 const i = f.indexOf(document.activeElement as HTMLElement);
150 if (e.shiftKey && i <= 0) {
151 e.preventDefault();
152 f[f.length - 1].focus();
153 } else if (!e.shiftKey && i === f.length - 1) {
154 e.preventDefault();
155 f[0].focus();
156 }
157 }
158 };
159 const mq = matchMedia("(min-width: 760px)");
160 const onMq = () => mq.matches && setOpen(false);
161 document.addEventListener("keydown", onKey);
162 mq.addEventListener("change", onMq);
163 return () => {
164 document.removeEventListener("keydown", onKey);
165 mq.removeEventListener("change", onMq);
166 };
167 }, [open]);
168
169 const moveHover = (a: HTMLElement | null) => {
170 const h = hover.current;
171 if (!h) return;
172 if (!a) {
173 h.style.opacity = "0";
174 return;
175 }
176 h.style.opacity = "1";
177 h.style.width = `${a.offsetWidth}px`;
178 h.style.transform = `translateX(${a.offsetLeft}px)`;
179 };
180
181 const st = status.state ?? "ok";
182
183 return (
184 <header ref={root} className={`fd-fcn${className ? " " + className : ""}`} data-compact={compact ? "" : undefined} data-open={open ? "" : undefined}>
185 <style>{css}</style>
186 <div className="fd-fcn-shape" aria-hidden="true">
187 <span className="fd-fcn-edge" />
188 <span className="fd-fcn-fill" />
189 </div>
190 <div className="fd-fcn-row">
191 <div className="fd-fcn-l">
192 <a className="fd-fcn-logo" href="#top" aria-label={`${brand} home`}>
193 <span className="fd-fcn-mark">
194 <svg width="22" height="22" viewBox="0 0 22 22" aria-hidden="true">
195 <path d="M3 19V9.5L11 3l8 6.5V19" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
196 <path d="M7.5 19v-5.2a3.5 3.5 0 0 1 7 0V19" fill="none" stroke="currentColor" strokeWidth="1.8" />
197 <path d="M11 12.6c-1 1-1 2.2 0 3.2 1-1 1-2.2 0-3.2z" fill="var(--fd-fcn-accent)" />
198 </svg>
199 <i className="fd-fcn-markdot" data-state={st} />
200 </span>
201 <span className="fd-fcn-word">{brand}</span>
202 </a>
203 <span className="fd-fcn-fade fd-fcn-chip" aria-hidden={compact || undefined}>{version}</span>
204 <a className="fd-fcn-fade fd-fcn-status" href={status.href} tabIndex={compact ? -1 : undefined} aria-hidden={compact || undefined}>
205 <i className="fd-fcn-dot" data-state={st} />
206 {status.label}
207 </a>
208 </div>
209
210 <nav ref={navRef} className="fd-fcn-c" aria-label="Primary" onPointerLeave={() => moveHover(null)}>
211 <span ref={hover} className="fd-fcn-hover" aria-hidden="true" />
212 {links.map((l) => (
213 <a key={l.href} className="fd-fcn-link" href={l.href} onPointerEnter={(e) => moveHover(e.currentTarget)} onFocus={(e) => moveHover(e.currentTarget)} onBlur={() => moveHover(null)}>
214 {l.label}
215 {l.badge && <span className="fd-fcn-badge">{l.badge}</span>}
216 </a>
217 ))}
218 </nav>
219
220 <div className="fd-fcn-r">
221 <a className="fd-fcn-fade fd-fcn-gh" href="#github" tabIndex={compact ? -1 : undefined} aria-hidden={compact || undefined} aria-label={`GitHub, ${stars} stars`}>
222 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 .2a8 8 0 0 0-2.5 15.6c.4 0 .5-.2.5-.4v-1.5c-2.2.5-2.7-1-2.7-1-.4-.9-.9-1.2-.9-1.2-.7-.5.1-.5.1-.5.8.1 1.2.8 1.2.8.7 1.3 1.9.9 2.3.7.1-.5.3-.9.5-1.1-1.8-.2-3.6-.9-3.6-4 0-.9.3-1.6.8-2.1-.1-.2-.4-1 .1-2.1 0 0 .7-.2 2.2.8a7.4 7.4 0 0 1 4 0c1.5-1 2.2-.8 2.2-.8.4 1.1.2 1.9.1 2.1.5.6.8 1.3.8 2.1 0 3.1-1.9 3.7-3.6 3.9.3.3.6.8.6 1.5v2.2c0 .2.1.5.6.4A8 8 0 0 0 8 .2z" /></svg>
223 <span>{stars}</span>
224 </a>
225 <a className="fd-fcn-fade fd-fcn-ghost" href={signIn.href} tabIndex={compact ? -1 : undefined} aria-hidden={compact || undefined}>{signIn.label}</a>
226 <a className="fd-fcn-cta" href={cta.href}>
227 {cta.label}
228 <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 6h8M6.5 2.5 10 6 6.5 9.5" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
229 </a>
230 <button ref={burger} type="button" className="fd-fcn-burger" aria-expanded={open} aria-controls={sheetId} aria-label={open ? "Close menu" : "Open menu"} onClick={() => setOpen((o) => !o)}>
231 <span /><span />
232 </button>
233 </div>
234 </div>
235
236 {open && <div className="fd-fcn-scrim" aria-hidden="true" onClick={() => setOpen(false)} />}
237 <div ref={sheet} id={sheetId} className="fd-fcn-sheet" hidden={!open}>
238 <nav aria-label="Mobile">
239 {links.map((l, i) => (
240 <a key={l.href} href={l.href} className="fd-fcn-mlink" onClick={() => setOpen(false)}>
241 <span className="fd-fcn-mi">{String(i + 1).padStart(2, "0")}</span>
242 <span className="fd-fcn-ml">{l.label}</span>
243 {l.badge && <span className="fd-fcn-badge">{l.badge}</span>}
244 </a>
245 ))}
246 </nav>
247 <div className="fd-fcn-mfoot">
248 <a className="fd-fcn-status" href={status.href}><i className="fd-fcn-dot" data-state={st} />{status.label}</a>
249 <span className="fd-fcn-chip">{version}</span>
250 </div>
251 <div className="fd-fcn-mbtns">
252 <a className="fd-fcn-mghost" href={signIn.href}>{signIn.label}</a>
253 <a className="fd-fcn-mcta" href={cta.href}>{cta.label}</a>
254 </div>
255 </div>
256 </header>
257 );
258}
259
260const css = `
261.fd-fcn {
262 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
263 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
264 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
265 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
266 --fd-fcn-bg: #0d0e10;
267 --fd-fcn-cap: #17191c;
268 --fd-fcn-line: #26282d;
269 --fd-fcn-ink: #ecebe6;
270 --fd-fcn-dim: #8d8e93;
271 --fd-fcn-accent: #c8f06b;
272 --fcn-s: 0px; --fcn-t: 0px; --fcn-r: 0px; --fcn-dx: 0px; --fcn-p: 0; --fcn-fade: 1;
273 position: sticky; top: 0; z-index: 50;
274 display: block; height: 76px; margin: 0; padding: 0;
275 font: 400 15px/1.4 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
276 color: var(--fd-fcn-ink); background: transparent; border: 0;
277}
278.fd-fcn, .fd-fcn *, .fd-fcn *::before, .fd-fcn *::after { box-sizing: border-box; }
279.fd-fcn-shape { position: absolute; inset: 0; z-index: 1; pointer-events: none; filter: drop-shadow(0 calc(var(--fcn-p) * 14px) calc(var(--fcn-p) * 22px) rgba(0,0,0,calc(var(--fcn-p) * 0.55))); }
280.fd-fcn-edge, .fd-fcn-fill { position: absolute; inset: 0; display: block; }
281.fd-fcn-edge {
282 background: var(--fd-fcn-line);
283 clip-path: inset(var(--fcn-t) var(--fcn-s) var(--fcn-t) var(--fcn-s) round var(--fcn-r));
284}
285.fd-fcn-fill {
286 background: color-mix(in srgb, var(--fd-fcn-cap) calc(var(--fcn-p) * 100%), var(--fd-fcn-bg));
287 clip-path: inset(calc(var(--fcn-t) + var(--fcn-p) * 1px) calc(var(--fcn-s) + var(--fcn-p) * 1px) calc(var(--fcn-t) + 1px) calc(var(--fcn-s) + var(--fcn-p) * 1px) round max(0px, calc(var(--fcn-r) - 1px)));
288}
289.fd-fcn-row { position: relative; height: 100%; display: flex; align-items: center; justify-content: space-between; padding: 0 32px; margin: 0; }
290.fd-fcn-l, .fd-fcn-r { position: relative; z-index: 2; display: flex; align-items: center; gap: 14px; margin: 0; padding: 0; }
291.fd-fcn-l { transform: translateX(var(--fcn-dx)); }
292.fd-fcn-r { transform: translateX(calc(var(--fcn-dx) * -1)); gap: 8px; }
293.fd-fcn-fade { opacity: var(--fcn-fade); }
294.fd-fcn[data-compact] .fd-fcn-fade { visibility: hidden; transition: visibility 0s linear var(--fd-dur-2); }
295
296.fd-fcn-logo { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 4px 6px 4px 4px; border-radius: 10px; color: var(--fd-fcn-ink); text-decoration: none; background: none; }
297.fd-fcn-mark { position: relative; display: block; width: 22px; height: 22px; }
298.fd-fcn-mark svg { display: block; }
299.fd-fcn-markdot { position: absolute; right: -3px; top: -2px; width: 7px; height: 7px; border-radius: 9999px; background: #4ade80; box-shadow: 0 0 0 2px var(--fd-fcn-cap); opacity: var(--fcn-p); transform: scale(calc(0.4 + var(--fcn-p) * 0.6)); }
300.fd-fcn-word { font: 650 1.25rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: -0.04em; color: var(--fd-fcn-ink); }
301.fd-fcn-chip { display: inline-block; margin: 0; padding: 3px 7px; border: 1px solid var(--fd-fcn-line); border-radius: 6px; font: 500 0.75rem/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0; color: var(--fd-fcn-dim); background: #141518; }
302.fd-fcn-status { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 4px 8px; border-radius: 9999px; font: 500 0.8125rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; color: var(--fd-fcn-dim); text-decoration: none; transition: color var(--fd-dur-1) var(--fd-ease-out), background var(--fd-dur-1) var(--fd-ease-out); }
303.fd-fcn-status:hover { color: var(--fd-fcn-ink); background: #17191c; }
304.fd-fcn-dot { position: relative; display: block; width: 7px; height: 7px; border-radius: 9999px; background: #4ade80; }
305.fd-fcn-dot::after { content: ""; position: absolute; inset: -4px; border-radius: 9999px; border: 1.5px solid #4ade80; opacity: 0; animation: fd-fcn-ping 2.4s var(--fd-ease-out) infinite; }
306.fd-fcn-dot[data-state="degraded"], .fd-fcn-markdot[data-state="degraded"] { background: #f5b83d; }
307.fd-fcn-dot[data-state="down"], .fd-fcn-markdot[data-state="down"] { background: #f2555a; }
308.fd-fcn-dot[data-state="degraded"]::after { border-color: #f5b83d; }
309.fd-fcn-dot[data-state="down"]::after { border-color: #f2555a; }
310@keyframes fd-fcn-ping { 0% { transform: scale(0.5); opacity: 0.9; } 70%, 100% { transform: scale(1.6); opacity: 0; } }
311
312.fd-fcn-c { position: absolute; left: 50%; top: 50%; z-index: 2; transform: translate(-50%, -50%); display: flex; align-items: center; gap: 2px; margin: 0; padding: 0; }
313.fd-fcn-hover { position: absolute; left: 0; top: 0; height: 100%; width: 0; border-radius: 9999px; background: #23252a; opacity: 0; pointer-events: none; transition: transform var(--fd-dur-2) var(--fd-ease-out), width var(--fd-dur-2) var(--fd-ease-out), opacity var(--fd-dur-1) linear; }
314.fd-fcn-link { position: relative; display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 8px 13px; border-radius: 9999px; font: 500 0.875rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.005em; color: #bdbcb7; text-decoration: none; white-space: nowrap; transition: color var(--fd-dur-1) var(--fd-ease-out); }
315.fd-fcn-link:hover, .fd-fcn-link:focus-visible { color: var(--fd-fcn-ink); }
316.fd-fcn-badge { display: inline-block; margin: 0; padding: 2px 5px; border-radius: 4px; font: 600 0.625rem/1.1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: 0.04em; text-transform: uppercase; color: #0d0e10; background: var(--fd-fcn-accent); }
317
318.fd-fcn-gh { display: inline-flex; align-items: center; gap: 7px; margin: 0; padding: 8px 10px; border-radius: 9999px; font: 500 0.8125rem/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: var(--fd-fcn-dim); text-decoration: none; transition: color var(--fd-dur-1) var(--fd-ease-out); }
319.fd-fcn-gh svg { display: block; }
320.fd-fcn-gh:hover { color: var(--fd-fcn-ink); }
321.fd-fcn-ghost { display: inline-flex; margin: 0; padding: 8px 12px; border-radius: 9999px; font: 500 0.875rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; color: var(--fd-fcn-ink); text-decoration: none; }
322.fd-fcn-ghost:hover { background: #1c1e22; }
323.fd-fcn-cta { display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 9px 14px 9px 16px; border-radius: 9999px; font: 600 0.875rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.005em; color: #0d0e10; background: var(--fd-fcn-accent); text-decoration: none; white-space: nowrap; transition: transform var(--fd-dur-1) var(--fd-ease-out), background var(--fd-dur-1) var(--fd-ease-out); }
324.fd-fcn-cta svg { display: block; transition: transform var(--fd-dur-2) var(--fd-spring); }
325.fd-fcn-cta:hover { background: #d6f78b; }
326.fd-fcn-cta:hover svg { transform: translateX(3px); }
327.fd-fcn-cta:active { transform: scale(0.96); transition: transform var(--fd-dur-1) var(--fd-ease-out); }
328.fd-fcn a:focus-visible, .fd-fcn button:focus-visible { outline: 2px solid color-mix(in srgb, var(--fd-fcn-accent) 70%, transparent); outline-offset: 2px; }
329
330.fd-fcn-burger { display: none; position: relative; width: 40px; height: 40px; margin: 0; padding: 0; border: 1px solid var(--fd-fcn-line); border-radius: 9999px; background: #15171a; color: var(--fd-fcn-ink); cursor: pointer; appearance: none; -webkit-appearance: none; }
331.fd-fcn-burger span { position: absolute; left: 12px; right: 12px; height: 1.5px; border-radius: 2px; background: currentColor; transition: transform var(--fd-dur-3) var(--fd-spring); }
332.fd-fcn-burger span:nth-child(1) { top: 16px; }
333.fd-fcn-burger span:nth-child(2) { top: 22px; }
334.fd-fcn[data-open] .fd-fcn-burger span:nth-child(1) { transform: translateY(3px) rotate(45deg); }
335.fd-fcn[data-open] .fd-fcn-burger span:nth-child(2) { transform: translateY(-3px) rotate(-45deg); }
336
337.fd-fcn-scrim { position: fixed; inset: 0; z-index: 0; background: rgba(5,6,7,0.6); animation: fd-fcn-fade var(--fd-dur-2) linear; }
338@keyframes fd-fcn-fade { from { opacity: 0; } }
339.fd-fcn-sheet { position: absolute; left: 12px; right: 12px; top: 70px; z-index: 3; margin: 0; padding: 8px 8px 12px; border: 1px solid var(--fd-fcn-line); border-radius: 24px; background: var(--fd-fcn-cap); box-shadow: 0 24px 60px rgba(0,0,0,0.55); animation: fd-fcn-drop var(--fd-dur-3) var(--fd-ease-out); }
340.fd-fcn-sheet[hidden] { display: none; }
341@keyframes fd-fcn-drop { from { clip-path: inset(0 0 100% 0 round 24px); } to { clip-path: inset(0 0 0 0 round 24px); } }
342.fd-fcn-sheet nav { display: block; margin: 0; padding: 0; }
343.fd-fcn-mlink { display: flex; align-items: baseline; gap: 14px; margin: 0; padding: 14px 14px; border-radius: 16px; color: var(--fd-fcn-ink); text-decoration: none; animation: fd-fcn-rise var(--fd-dur-3) var(--fd-ease-out) both; }
344.fd-fcn-mlink:nth-child(1) { animation-delay: 40ms; } .fd-fcn-mlink:nth-child(2) { animation-delay: 80ms; } .fd-fcn-mlink:nth-child(3) { animation-delay: 120ms; } .fd-fcn-mlink:nth-child(4) { animation-delay: 160ms; } .fd-fcn-mlink:nth-child(5) { animation-delay: 200ms; }
345@keyframes fd-fcn-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
346.fd-fcn-mlink:active { background: #1f2125; }
347.fd-fcn-mi { font: 500 0.75rem/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: var(--fd-fcn-dim); }
348.fd-fcn-ml { flex: 1; font: 600 1.5rem/1.1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.025em; color: var(--fd-fcn-ink); }
349.fd-fcn-mfoot { display: flex; align-items: center; justify-content: space-between; margin: 8px 6px 10px; padding: 12px 0 0; border-top: 1px solid var(--fd-fcn-line); }
350.fd-fcn-mbtns { display: grid; grid-template-columns: 1fr 1.4fr; gap: 8px; margin: 0; padding: 0; }
351.fd-fcn-mghost, .fd-fcn-mcta { display: block; margin: 0; padding: 14px 10px; border-radius: 9999px; text-align: center; font: 600 0.9375rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; text-decoration: none; }
352.fd-fcn-mghost { color: var(--fd-fcn-ink); border: 1px solid var(--fd-fcn-line); background: transparent; }
353.fd-fcn-mcta { color: #0d0e10; background: var(--fd-fcn-accent); border: 1px solid var(--fd-fcn-accent); }
354
355@media (max-width: 1080px) { .fd-fcn-gh, .fd-fcn-row .fd-fcn-status { display: none; } }
356@media (max-width: 759px) {
357 .fd-fcn { height: 68px; }
358 .fd-fcn-row { padding: 0 20px; }
359 .fd-fcn-c, .fd-fcn-ghost, .fd-fcn-cta { display: none; }
360 .fd-fcn-burger { display: block; }
361 .fd-fcn-sheet { top: 64px; }
362}
363@media (prefers-reduced-motion: reduce) {
364 .fd-fcn-dot::after { animation: none; }
365 .fd-fcn-sheet, .fd-fcn-mlink { animation: none; }
366 .fd-fcn-burger span, .fd-fcn-hover, .fd-fcn-cta svg { transition: none; }
367}
368@media (prefers-contrast: more) { .fd-fcn { --fd-fcn-line: #5a5c63; --fd-fcn-dim: #b7b8bc; } }
369`;
370
371/* ── Demo: Kiln changelog page ─────────────────────────────────────────── */
372
373const ENTRIES = [
374 {
375 date: "Oct 29, 2026", tag: "v4.2.0", title: "Remote cache now shards by package graph",
376 body: "Cache keys used to hash the whole lockfile, so bumping one dev dependency invalidated every package. Keys are now scoped to each package's resolved subgraph. On our own monorepo (312 packages) median CI time went from 11m 40s to 3m 05s.",
377 items: ["kiln cache stats --since 7d shows hit rate per package", "New KILN_CACHE_SHARD env for self-hosted buckets", "Fixed a race when two runners uploaded the same artifact"],
378 },
379 {
380 date: "Oct 14, 2026", tag: "v4.1.3", title: "Quieter logs, louder failures",
381 body: "Successful tasks now collapse to one line. Failed tasks print their full output, the exact command, and the cache key that missed, so you can rerun it locally with kiln replay.",
382 items: ["kiln replay <task-id> reproduces a CI task on your machine", "Log output is 74% shorter on green builds"],
383 },
384 {
385 date: "Sep 30, 2026", tag: "v4.1.0", title: "Bun and Deno workspaces",
386 body: "Kiln reads bun.lock and deno.json workspaces natively. No adapter, no config. Mixed repos work too: we tested a 40 package repo with pnpm, Bun and a Rust crate side by side.",
387 items: ["Workspace detection takes 38ms on a cold start", "Rust crates hash Cargo.lock per member"],
388 },
389];
390
391export function Demo() {
392 return (
393 <div className="fd-fcn-demo">
394 <style>{demoCss}</style>
395 <FloatingCapsuleNav />
396 <main className="fd-fcn-demo-main">
397 <div className="fd-fcn-demo-head">
398 <p className="fd-fcn-demo-eyebrow">Changelog · RSS · 214 releases since 2022</p>
399 <h1>What we shipped.</h1>
400 <p className="fd-fcn-demo-lede">Every release of the kiln CLI and remote cache, newest first. Breaking changes are marked, and there have been three.</p>
401 </div>
402 {ENTRIES.map((e) => (
403 <article key={e.tag} className="fd-fcn-demo-entry">
404 <div className="fd-fcn-demo-meta">
405 <time>{e.date}</time>
406 <code>{e.tag}</code>
407 </div>
408 <div className="fd-fcn-demo-body">
409 <h2>{e.title}</h2>
410 <p>{e.body}</p>
411 <pre aria-label="Terminal"><span>$</span> npx kiln@{e.tag.slice(1)} run build --affected{"\n"}<em>✓ 41 tasks · 37 cached · 3m 05s</em></pre>
412 <ul>{e.items.map((i) => <li key={i}>{i}</li>)}</ul>
413 </div>
414 </article>
415 ))}
416 </main>
417 </div>
418 );
419}
420
421const demoCss = `
422.fd-fcn-demo { margin: 0; padding: 0; min-height: 100vh; background: #0d0e10; color: #ecebe6; font: 400 15px/1.6 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; }
423.fd-fcn-demo *, .fd-fcn-demo *::before, .fd-fcn-demo *::after { box-sizing: border-box; }
424.fd-fcn-demo-main { max-width: 1040px; margin: 0 auto; padding: 72px 32px 160px; }
425.fd-fcn-demo-head { margin: 0 0 72px; padding: 0 0 40px; border-bottom: 1px solid #26282d; }
426.fd-fcn-demo-eyebrow { margin: 0 0 18px; font: 500 0.8125rem/1.4 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: #8d8e93; }
427.fd-fcn-demo-head h1 { margin: 0; font: 650 clamp(2.75rem, 7vw, 5.5rem)/0.95 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.045em; color: #ecebe6; }
428.fd-fcn-demo-lede { max-width: 520px; margin: 22px 0 0; font: 400 1.0625rem/1.55 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; color: #a2a3a8; }
429.fd-fcn-demo-entry { display: grid; grid-template-columns: 200px 1fr; gap: 32px; margin: 0 0 72px; padding: 0; }
430.fd-fcn-demo-meta { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; position: sticky; top: 96px; align-self: start; }
431.fd-fcn-demo-meta time { font: 500 0.8125rem/1.4 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: #8d8e93; }
432.fd-fcn-demo-meta code { padding: 3px 7px; border-radius: 6px; font: 600 0.75rem/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: #0d0e10; background: #c8f06b; }
433.fd-fcn-demo-body h2 { margin: 0 0 12px; font: 600 1.5rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.02em; color: #ecebe6; }
434.fd-fcn-demo-body p { max-width: 640px; margin: 0 0 18px; color: #a2a3a8; }
435.fd-fcn-demo-body pre { margin: 0 0 18px; padding: 16px 18px; border: 1px solid #26282d; border-radius: 12px; background: #121316; font: 400 0.8125rem/1.7 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; color: #d5d4cf; overflow-x: auto; white-space: pre; }
436.fd-fcn-demo-body pre span { color: #c8f06b; }
437.fd-fcn-demo-body pre em { font-style: normal; color: #4ade80; }
438.fd-fcn-demo-body ul { margin: 0; padding: 0; list-style: none; }
439.fd-fcn-demo-body li { position: relative; margin: 0; padding: 6px 0 6px 20px; color: #c9c8c3; border-top: 1px dashed #24262a; }
440.fd-fcn-demo-body li::before { content: "+"; position: absolute; left: 0; top: 6px; font-family: ui-monospace, "SF Mono", Menlo, monospace; color: #c8f06b; }
441@media (max-width: 759px) {
442 .fd-fcn-demo-main { padding: 48px 20px 120px; }
443 .fd-fcn-demo-entry { grid-template-columns: 1fr; gap: 14px; margin-bottom: 56px; }
444 .fd-fcn-demo-meta { position: static; flex-direction: row; align-items: center; }
445}
446`;

More Navbars. Same file contract.

Command Nav, navbars preview: Navbar with command palette: search opens a real ⌘K palette with recents, grouped fuzzy results and a live…

Command Nav

Navbar with command palette: search opens a real ⌘K palette with recents, grouped fuzzy results and a live preview.

App Sidebar Nav, navbars preview: App sidebar navigation for React: workspace switcher, nested groups, badges, a sliding active marker and a…

App Sidebar Nav

App sidebar navigation for React: workspace switcher, nested groups, badges, a sliding active marker and a collapsible icon rail.

Island Nav, navbars preview: Dynamic island navbar: a dark pill that spring-morphs between links, a now-playing mini player, search and a…

Island Nav

Dynamic island navbar: a dark pill that spring-morphs between links, a now-playing mini player, search and a phone menu.

Questions about Floating Capsule Nav

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

Sticky navbar that springs from a full-width bar into a floating capsule on scroll, with a live status light and version chip. 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 (clip-path inset with round, color-mix: Chrome 111+, Safari 16.2+, Firefox 113+).

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.