Mobile Slide Navbar
Original · freenavbar with a mobile slide menu
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/navx-mobile-slide.json"use client";
import {
useCallback,
useEffect,
useId,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
} from "react";
import {
AnimatePresence,
motion,
useReducedMotion,
type Transition,
} from "motion/react";
type NavChild = { label: string; href: string; desc: string };
type NavGroup = { id: string; label: string; items: NavChild[] };
type NavLink = { label: string; href: string; badge?: string };
const GROUPS: NavGroup[] = [
{
id: "platform",
label: "Platform",
items: [
{
label: "Live Metrics",
href: "#metrics",
desc: "Sub-second dashboards across every service.",
},
{
label: "Distributed Tracing",
href: "#tracing",
desc: "Follow a request from the edge to the database.",
},
{
label: "Log Explorer",
href: "#logs",
desc: "Search billions of lines with zero sampling.",
},
{
label: "On-call & Alerting",
href: "#alerting",
desc: "Route incidents to the right engineer in seconds.",
},
],
},
{
id: "solutions",
label: "Solutions",
items: [
{
label: "For Engineering",
href: "#engineering",
desc: "Cut mean-time-to-resolution roughly in half.",
},
{
label: "For Platform Teams",
href: "#platform-teams",
desc: "One control plane for every cluster you run.",
},
{
label: "For SRE",
href: "#sre",
desc: "Error budgets and SLOs that actually hold.",
},
],
},
];
const DIRECT_LINKS: NavLink[] = [
{ label: "Pricing", href: "#pricing" },
{ label: "Docs", href: "#docs" },
{ label: "Changelog", href: "#changelog", badge: "New" },
];
const DESKTOP_LINKS: NavLink[] = [
{ label: "Platform", href: "#platform" },
{ label: "Solutions", href: "#solutions" },
...DIRECT_LINKS,
];
function SignalMark() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-5 w-5"
>
<circle cx="12" cy="12" r="2" fill="currentColor" stroke="none" />
<path d="M7.7 7.7a6 6 0 0 0 0 8.6" />
<path d="M16.3 16.3a6 6 0 0 0 0-8.6" />
<path d="M4.9 4.9a10 10 0 0 0 0 14.2" />
<path d="M19.1 19.1a10 10 0 0 0 0-14.2" />
</svg>
);
}
function MenuIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
aria-hidden="true"
className="h-5 w-5"
>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
}
function CloseIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
aria-hidden="true"
className="h-5 w-5"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
);
}
function ChevronIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4"
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function ArrowIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
}
function Badge({ children }: { children: string }) {
return (
<span className="inline-flex items-center rounded-full bg-amber-100 px-1.5 py-0.5 text-[0.625rem] font-semibold uppercase tracking-wide text-amber-700 ring-1 ring-inset ring-amber-300/60 dark:bg-amber-400/15 dark:text-amber-300 dark:ring-amber-400/30">
{children}
</span>
);
}
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
export default function NavxMobileSlide() {
const reduce = !!useReducedMotion();
const uid = useId();
const panelId = `${uid}-panel`;
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [openGroup, setOpenGroup] = useState<string | null>("platform");
const toggleRef = useRef<HTMLButtonElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
const closeMenu = useCallback(() => {
setOpen(false);
toggleRef.current?.focus();
}, []);
// Scrolled state via IntersectionObserver (no scroll listener).
useEffect(() => {
const el = sentinelRef.current;
if (!el) return;
const io = new IntersectionObserver(
([entry]) => setScrolled(!entry.isIntersecting),
{ threshold: 0 },
);
io.observe(el);
return () => io.disconnect();
}, []);
// Lock body scroll while the slide menu is open.
useEffect(() => {
if (!open) return;
const previous = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = previous;
};
}, [open]);
// Move focus into the panel on open.
useEffect(() => {
if (!open) return;
const id = window.requestAnimationFrame(() => closeRef.current?.focus());
return () => window.cancelAnimationFrame(id);
}, [open]);
// Close the menu when the viewport grows to desktop.
useEffect(() => {
const mq = window.matchMedia("(min-width: 768px)");
const onChange = (event: MediaQueryListEvent) => {
if (event.matches) setOpen(false);
};
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
const onPanelKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>) => {
if (event.key === "Escape") {
event.preventDefault();
closeMenu();
return;
}
if (event.key !== "Tab") return;
const panel = panelRef.current;
if (!panel) return;
const focusable = Array.from(
panel.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
),
).filter((node) => node.offsetParent !== null || node === closeRef.current);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement;
if (event.shiftKey && active === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && active === last) {
event.preventDefault();
first.focus();
}
},
[closeMenu],
);
const panelTransition: Transition = reduce
? { duration: 0 }
: { type: "spring", stiffness: 300, damping: 32, mass: 0.9 };
const fadeTransition: Transition = { duration: reduce ? 0 : 0.2, ease: "easeOut" };
const collapseTransition: Transition = {
duration: reduce ? 0 : 0.24,
ease: "easeInOut",
};
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 text-slate-900 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-100">
<style>{`
@keyframes navxms-ping {
0% { transform: scale(1); opacity: 0.5; }
70% { transform: scale(2.3); opacity: 0; }
100% { transform: scale(2.3); opacity: 0; }
}
@keyframes navxms-drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -20px, 0) scale(1.06); }
}
.navxms-ping { animation: navxms-ping 2.8s ease-out infinite; }
.navxms-drift { animation: navxms-drift 10s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.navxms-ping, .navxms-drift { animation: none !important; }
}
`}</style>
{/* Sentinel drives the scrolled/blur state without a scroll listener. */}
<div ref={sentinelRef} aria-hidden="true" className="absolute top-0 h-px w-full" />
{/* Ambient background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<span
style={{ animationDuration: "9s" }}
className="navxms-drift absolute -left-16 -top-24 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/15"
/>
<span
style={{ animationDuration: "12s" }}
className="navxms-drift absolute -right-10 top-40 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/15"
/>
</div>
{/* ---------------------------------------------------------------- */}
{/* NAVBAR */}
{/* ---------------------------------------------------------------- */}
<header
className={[
"sticky top-0 z-40 w-full transition-colors duration-300",
scrolled
? "border-b border-slate-200/70 bg-white/80 shadow-sm backdrop-blur-lg dark:border-slate-800/70 dark:bg-slate-950/80"
: "border-b border-transparent bg-transparent",
].join(" ")}
>
<nav
aria-label="Primary"
className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8"
>
<a
href="#top"
className={`group inline-flex items-center gap-2.5 rounded-lg py-1 pr-2 ${focusRing}`}
>
<span className="relative inline-flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-600 text-white shadow-sm shadow-indigo-600/30 dark:bg-indigo-500">
<span
aria-hidden="true"
className="navxms-ping absolute inset-0 rounded-xl bg-indigo-500/70"
/>
<span className="relative">
<SignalMark />
</span>
</span>
<span className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
Beacon
</span>
</a>
{/* Desktop links */}
<ul className="hidden items-center gap-1 md:flex">
{DESKTOP_LINKS.map((link) => (
<li key={link.label}>
<a
href={link.href}
className={`group relative inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-300 dark:hover:text-white ${focusRing}`}
>
{link.label}
{link.badge ? <Badge>{link.badge}</Badge> : null}
<span className="absolute inset-x-3 -bottom-0.5 h-px origin-left scale-x-0 bg-indigo-500 transition-transform duration-200 group-hover:scale-x-100 dark:bg-indigo-400" />
</a>
</li>
))}
</ul>
{/* Desktop actions */}
<div className="hidden items-center gap-2 md:flex">
<a
href="#signin"
className={`rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-300 dark:hover:text-white ${focusRing}`}
>
Sign in
</a>
<a
href="#trial"
className={`inline-flex items-center gap-1.5 rounded-full bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${focusRing}`}
>
Start free trial
<ArrowIcon />
</a>
</div>
{/* Mobile toggle */}
<button
ref={toggleRef}
type="button"
onClick={() => setOpen(true)}
aria-expanded={open}
aria-controls={panelId}
aria-label="Open main menu"
className={`inline-flex h-11 w-11 items-center justify-center rounded-xl border border-slate-200 bg-white/70 text-slate-700 transition-colors hover:bg-white active:scale-95 dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-200 dark:hover:bg-slate-900 md:hidden ${focusRing}`}
>
<MenuIcon />
</button>
</nav>
</header>
{/* ---------------------------------------------------------------- */}
{/* MOBILE SLIDE MENU */}
{/* ---------------------------------------------------------------- */}
<AnimatePresence>
{open ? (
<div className="fixed inset-0 z-50 md:hidden" role="presentation">
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={fadeTransition}
onClick={closeMenu}
aria-hidden="true"
className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm"
/>
<motion.div
key="panel"
ref={panelRef}
role="dialog"
aria-modal="true"
aria-label="Main menu"
id={panelId}
onKeyDown={onPanelKeyDown}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={panelTransition}
className="absolute right-0 top-0 flex h-full w-[min(88vw,22rem)] flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-slate-800 dark:bg-slate-950"
>
<div className="flex items-center justify-between border-b border-slate-200 px-5 py-4 dark:border-slate-800">
<span className="inline-flex items-center gap-2.5">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-600 text-white dark:bg-indigo-500">
<SignalMark />
</span>
<span className="text-base font-semibold tracking-tight">
Beacon
</span>
</span>
<button
ref={closeRef}
type="button"
onClick={closeMenu}
aria-label="Close menu"
className={`inline-flex h-10 w-10 items-center justify-center rounded-xl text-slate-600 transition-colors hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 ${focusRing}`}
>
<CloseIcon />
</button>
</div>
<div className="flex-1 overflow-y-auto overscroll-contain px-3 py-4">
<ul className="flex flex-col gap-1">
{GROUPS.map((group) => {
const isOpen = openGroup === group.id;
const regionId = `${uid}-${group.id}`;
return (
<li key={group.id}>
<button
type="button"
aria-expanded={isOpen}
aria-controls={regionId}
onClick={() =>
setOpenGroup((current) =>
current === group.id ? null : group.id,
)
}
className={`flex w-full items-center justify-between rounded-xl px-3 py-3 text-left text-base font-semibold text-slate-800 transition-colors hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-900 ${focusRing}`}
>
{group.label}
<span
className={`text-slate-400 transition-transform duration-200 dark:text-slate-500 ${
isOpen ? "rotate-180" : "rotate-0"
}`}
>
<ChevronIcon />
</span>
</button>
<AnimatePresence initial={false}>
{isOpen ? (
<motion.div
key="region"
id={regionId}
role="region"
aria-label={group.label}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={collapseTransition}
className="overflow-hidden"
>
<ul className="flex flex-col gap-0.5 py-1 pl-3 pr-1">
{group.items.map((item) => (
<li key={item.label}>
<a
href={item.href}
onClick={() => setOpen(false)}
className={`group flex flex-col gap-0.5 rounded-lg border-l-2 border-transparent px-3 py-2.5 transition-colors hover:border-indigo-500 hover:bg-slate-50 dark:hover:border-indigo-400 dark:hover:bg-slate-900 ${focusRing}`}
>
<span className="flex items-center gap-1 text-sm font-medium text-slate-800 dark:text-slate-100">
{item.label}
<span className="translate-x-0 text-indigo-500 opacity-0 transition-all duration-200 group-hover:translate-x-0.5 group-hover:opacity-100 dark:text-indigo-400">
<ArrowIcon />
</span>
</span>
<span className="text-xs leading-snug text-slate-500 dark:text-slate-400">
{item.desc}
</span>
</a>
</li>
))}
</ul>
</motion.div>
) : null}
</AnimatePresence>
</li>
);
})}
<li className="my-1 h-px bg-slate-200 dark:bg-slate-800" aria-hidden="true" />
{DIRECT_LINKS.map((link) => (
<li key={link.label}>
<a
href={link.href}
onClick={() => setOpen(false)}
className={`flex items-center justify-between rounded-xl px-3 py-3 text-base font-semibold text-slate-800 transition-colors hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-900 ${focusRing}`}
>
<span className="inline-flex items-center gap-2">
{link.label}
{link.badge ? <Badge>{link.badge}</Badge> : null}
</span>
<span className="text-slate-400 dark:text-slate-500">
<ArrowIcon />
</span>
</a>
</li>
))}
</ul>
</div>
<div className="grid gap-2 border-t border-slate-200 px-5 py-4 dark:border-slate-800">
<a
href="#signin"
onClick={() => setOpen(false)}
className={`inline-flex items-center justify-center rounded-xl border border-slate-300 px-4 py-2.5 text-sm font-semibold text-slate-800 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-900 ${focusRing}`}
>
Sign in
</a>
<a
href="#trial"
onClick={() => setOpen(false)}
className={`inline-flex items-center justify-center gap-1.5 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${focusRing}`}
>
Start free trial
<ArrowIcon />
</a>
</div>
</motion.div>
</div>
) : null}
</AnimatePresence>
{/* ---------------------------------------------------------------- */}
{/* PAGE CONTENT (demonstrates the sticky/scrolled navbar) */}
{/* ---------------------------------------------------------------- */}
<div id="top" className="relative mx-auto max-w-6xl px-4 pb-24 pt-16 sm:px-6 sm:pt-20 lg:px-8 lg:pt-28">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" />
Now with OpenTelemetry-native ingest
</span>
<h1 className="mt-6 text-balance text-4xl font-semibold tracking-tight text-slate-900 sm:text-5xl dark:text-white">
See every signal before your users do.
</h1>
<p className="mx-auto mt-5 max-w-xl text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300">
Beacon unifies metrics, traces, and logs in one fast, honest view,
so your team ships with confidence and sleeps through the night.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href="#trial"
className={`inline-flex w-full items-center justify-center gap-1.5 rounded-full bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 sm:w-auto dark:bg-indigo-500 dark:hover:bg-indigo-400 ${focusRing}`}
>
Start free trial
<ArrowIcon />
</a>
<a
href="#demo"
className={`inline-flex w-full items-center justify-center rounded-full border border-slate-300 bg-white/70 px-6 py-3 text-sm font-semibold text-slate-800 transition-colors hover:bg-white sm:w-auto dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-100 dark:hover:bg-slate-900 ${focusRing}`}
>
Book a demo
</a>
</div>
<p className="mt-4 text-xs text-slate-500 dark:text-slate-400">
No credit card required. 14-day trial on every plan.
</p>
</div>
<div className="mt-20 grid gap-4 sm:grid-cols-3">
{[
{
title: "Zero-sample logs",
body: "Keep every line searchable. Query billions of events without pre-aggregating away the detail you need at 3 a.m.",
},
{
title: "Traces that connect",
body: "Jump from a slow endpoint to the exact database span in two clicks. No copy-pasting request IDs between tools.",
},
{
title: "Alerts you trust",
body: "Tune noise down with SLO-aware thresholds so on-call only wakes for the incidents that genuinely matter.",
},
].map((card) => (
<div
key={card.title}
className="rounded-2xl border border-slate-200 bg-white/60 p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/50"
>
<h2 className="text-base font-semibold text-slate-900 dark:text-white">
{card.title}
</h2>
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{card.body}
</p>
</div>
))}
</div>
<p className="mt-16 text-center text-sm text-slate-500 dark:text-slate-400">
Scroll to watch the navbar settle into its glass state. On mobile, tap
the menu to open the slide-in panel.
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Centred Logo Navbar
OriginalA three-column navbar with the brand logo centred between the primary links and the account actions, wrapped in a soft rounded card.

Left Logo Navbar With CTA
OriginalA classic left-aligned logo navbar with centred navigation links and a prominent pill call-to-action button on the right.

Navbar With Dropdown Menu
OriginalA navbar featuring a JS-free product dropdown built with native details and summary, revealing a two-item mega-menu panel with icons.

Glass Transparent Navbar
OriginalA frosted glass navbar floating over a colourful gradient hero backdrop, using backdrop blur and translucent borders for a modern overlay header.

Responsive Navbar with Mobile Menu
MITA marketing site header with a logo, inline nav links, login/register buttons, and a working hamburger toggle that reveals a stacked mobile menu below the md breakpoint. Fully keyboard/ARIA accessible with light and dark variants.

Underline Tabs
MITAn accessible underline-style tab switcher with a bottom-border active indicator and a live content panel. State-driven with proper role=tablist/tab/tabpanel and aria-selected wiring, styled for light and dark modes.

Split Button Dropdown Menu
MITA split-button control with a chevron trigger that opens a divided action menu, including a destructive delete item. Closes on outside-click and Escape, with a rotating chevron and full ARIA menu roles for both light and dark themes.

Glass Navbar
Originalglassmorphic sticky navbar

Mega Navbar
Originalnavbar with a mega dropdown

Centered Logo Navbar
Originalnavbar with centred logo and split links

Sidebar Toggle Navbar
Originalnavbar with a sidebar toggle

Search Navbar
Originalnavbar with an expanding search

