Pill Navbar
Original · freefloating pill navbar
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-pill.json"use client";
import { useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type NavItem = {
id: string;
label: string;
};
type Section = {
id: string;
eyebrow: string;
title: string;
body: string;
};
const NAV_ITEMS: NavItem[] = [
{ id: "overview", label: "Overview" },
{ id: "features", label: "Features" },
{ id: "pricing", label: "Pricing" },
{ id: "customers", label: "Customers" },
{ id: "faq", label: "FAQ" },
];
const SECTIONS: Section[] = [
{
id: "overview",
eyebrow: "Deploy intelligence",
title: "Ship with a steady pulse",
body: "Halcyon collapses every pipeline, rollback, and on-call ping into one quiet timeline your whole team can read at a glance — no dashboards to babysit.",
},
{
id: "features",
eyebrow: "What you get",
title: "Three tools, zero war rooms",
body: "A flight recorder for every deploy, blast-radius maps before you merge, and alert grouping that mutes the noise so the one signal that matters gets through.",
},
{
id: "pricing",
eyebrow: "Fair by design",
title: "Pricing that scales down, not just up",
body: "Start free for small teams. Pay per active developer only when you grow — no seat minimums, no annual lock-in, and every plan keeps 30 days of deploy history.",
},
{
id: "customers",
eyebrow: "Proof",
title: "Teams that stopped dreading Fridays",
body: "From two-person seed startups to platform teams shipping 400 times a day, Halcyon runs quietly underneath the release process nobody wants to think about.",
},
{
id: "faq",
eyebrow: "Details",
title: "The questions we get most",
body: "How it connects, what it can see, and what happens at 2am when a deploy goes sideways. Short answers, no sales gloss.",
},
];
const FEATURES = [
{
title: "Flight recorder",
copy: "Every deploy captured frame by frame, so a 2am rollback takes seconds, not a five-person incident call.",
},
{
title: "Blast-radius maps",
copy: "See exactly which services a change touches before you merge — not after the pager starts buzzing.",
},
{
title: "Quiet by default",
copy: "Smart grouping mutes duplicate alerts and surfaces the single event that actually needs a human.",
},
];
const FAQS = [
{
q: "How long does it take to connect?",
a: "Around ten minutes. Halcyon reads from your existing CI and cloud provider through a scoped, read-only token — nothing to install on your servers.",
},
{
q: "What can it actually see?",
a: "Deploy events, service metadata, and health signals. It never reads application data, secrets, or customer records. You choose the scopes per environment.",
},
{
q: "Does it replace our incident tool?",
a: "No — it sits in front of it. Halcyon decides what deserves a page, then hands the clean context to whatever you already use for on-call.",
},
];
export default function NavxPill() {
const reduceMotion = useReducedMotion();
const [active, setActive] = useState<string>(NAV_ITEMS[0].id);
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const [searchOpen, setSearchOpen] = useState<boolean>(false);
const [query, setQuery] = useState<string>("");
const [elevated, setElevated] = useState<boolean>(false);
const panelId = useId();
const searchId = useId();
const toggleRef = useRef<HTMLButtonElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const firstMobileItemRef = useRef<HTMLAnchorElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Scroll-spy: mark the section nearest the top as active.
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActive(entry.target.id);
}
}
},
{ rootMargin: "-45% 0px -50% 0px", threshold: 0 },
);
for (const item of NAV_ITEMS) {
const el = document.getElementById(item.id);
if (el) observer.observe(el);
}
return () => observer.disconnect();
}, []);
// Elevate the pill once the page scrolls away from the very top.
useEffect(() => {
const node = sentinelRef.current;
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => setElevated(!entry.isIntersecting),
{ threshold: 0 },
);
observer.observe(node);
return () => observer.disconnect();
}, []);
// Global keys: "/" opens search, Escape closes overlays.
useEffect(() => {
function onKey(event: KeyboardEvent) {
const el = document.activeElement;
const typing =
el instanceof HTMLElement &&
(el.tagName === "INPUT" ||
el.tagName === "TEXTAREA" ||
el.isContentEditable);
if (event.key === "/" && !typing) {
event.preventDefault();
setSearchOpen(true);
} else if (event.key === "Escape") {
setSearchOpen(false);
setMenuOpen((open) => {
if (open) toggleRef.current?.focus();
return false;
});
}
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
// Close the mobile sheet on an outside pointer press.
useEffect(() => {
if (!menuOpen) return;
function onDown(event: PointerEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setMenuOpen(false);
}
}
window.addEventListener("pointerdown", onDown);
return () => window.removeEventListener("pointerdown", onDown);
}, [menuOpen]);
// Focus management for the expandable controls.
useEffect(() => {
if (searchOpen) searchInputRef.current?.focus();
}, [searchOpen]);
useEffect(() => {
if (menuOpen) firstMobileItemRef.current?.focus();
}, [menuOpen]);
function goTo(id: string) {
setActive(id);
setMenuOpen(false);
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({
behavior: reduceMotion ? "auto" : "smooth",
block: "start",
});
}
}
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-indigo-400 dark:focus-visible:ring-offset-slate-950";
const softTransition = reduceMotion
? { duration: 0 }
: { duration: 0.18, ease: "easeOut" as const };
return (
<section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes navxpill-pulse {
0%, 100% { opacity: 0.35; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.12); }
}
@keyframes navxpill-sheen {
0% { transform: translateX(-130%); }
55%, 100% { transform: translateX(230%); }
}
.navxpill-pulse { animation: navxpill-pulse 3.8s ease-in-out infinite; }
.navxpill-sheen { animation: navxpill-sheen 5s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.navxpill-pulse, .navxpill-sheen { animation: none; }
}
`}</style>
{/* Ambient background wash */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-70 dark:opacity-100"
style={{
background:
"radial-gradient(60rem 32rem at 50% -8rem, rgba(99,102,241,0.14), transparent 70%)",
}}
/>
<div className="relative mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8">
<div ref={sentinelRef} aria-hidden="true" className="h-px w-full" />
{/* Floating pill navbar */}
<div ref={containerRef} className="sticky top-4 z-50">
<nav
aria-label="Primary"
className={[
"relative flex items-center justify-between gap-2 rounded-full border pl-3 pr-2 backdrop-blur-xl transition-all duration-300",
"border-slate-200/80 bg-white/80 dark:border-white/10 dark:bg-slate-900/70",
elevated
? "py-1.5 shadow-[0_18px_50px_-18px_rgba(15,23,42,0.45)] dark:shadow-[0_18px_60px_-20px_rgba(0,0,0,0.8)]"
: "py-2 shadow-[0_10px_34px_-16px_rgba(15,23,42,0.35)] dark:shadow-[0_10px_40px_-18px_rgba(0,0,0,0.7)]",
].join(" ")}
>
{/* Brand */}
<a
href="#overview"
onClick={(e) => {
e.preventDefault();
goTo("overview");
}}
className={`group flex shrink-0 items-center gap-2 rounded-full px-1.5 py-1 ${focusRing}`}
>
<span className="relative grid h-8 w-8 place-items-center">
<span
aria-hidden="true"
className="navxpill-pulse absolute inset-0 rounded-full bg-indigo-500/40 blur-md"
/>
<span className="relative grid h-8 w-8 place-items-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-inner">
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 13h4l2.5 6L14 5l2.5 8H21" />
</svg>
</span>
</span>
<span className="text-[15px] font-semibold tracking-tight text-slate-900 dark:text-white">
Halcyon
</span>
</a>
{/* Center links (desktop) */}
<ul className="hidden items-center gap-0.5 md:flex">
{NAV_ITEMS.map((item) => {
const isActive = active === item.id;
return (
<li key={item.id} className="relative">
<a
href={`#${item.id}`}
aria-current={isActive ? "location" : undefined}
onClick={(e) => {
e.preventDefault();
goTo(item.id);
}}
className={[
"relative flex items-center rounded-full px-3.5 py-2 text-sm font-medium transition-colors",
focusRing,
isActive
? "text-slate-900 dark:text-white"
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
].join(" ")}
>
{isActive && (
<motion.span
layoutId="navxpill-active"
aria-hidden="true"
className="absolute inset-0 -z-10 rounded-full bg-white shadow-sm ring-1 ring-slate-900/5 dark:bg-white/10 dark:ring-white/10"
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 420, damping: 34 }
}
/>
)}
{item.label}
</a>
</li>
);
})}
</ul>
{/* Right actions */}
<div className="flex shrink-0 items-center gap-1">
{/* Expandable search (desktop) */}
<div
role="search"
className="relative hidden items-center md:flex"
>
<AnimatePresence initial={false} mode="wait">
{searchOpen ? (
<motion.div
key="field"
initial={reduceMotion ? false : { width: 40, opacity: 0 }}
animate={{ width: 208, opacity: 1 }}
exit={reduceMotion ? undefined : { width: 40, opacity: 0 }}
transition={softTransition}
className="relative overflow-hidden"
>
<label htmlFor={searchId} className="sr-only">
Search Halcyon
</label>
<svg
viewBox="0 0 24 24"
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.2-3.2" />
</svg>
<input
ref={searchInputRef}
id={searchId}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onBlur={() => {
if (query.trim() === "") setSearchOpen(false);
}}
placeholder="Search docs, deploys…"
className={`w-full rounded-full border border-slate-200 bg-white/90 py-2 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 dark:border-white/10 dark:bg-slate-800/80 dark:text-white dark:placeholder:text-slate-500 ${focusRing}`}
/>
</motion.div>
) : (
<motion.button
key="button"
type="button"
onClick={() => setSearchOpen(true)}
aria-label="Open search"
initial={reduceMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={reduceMotion ? undefined : { opacity: 0 }}
transition={softTransition}
className={`grid h-9 w-9 place-items-center rounded-full text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-white/10 dark:hover:text-white ${focusRing}`}
>
<svg
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.2-3.2" />
</svg>
</motion.button>
)}
</AnimatePresence>
</div>
<a
href="#overview"
onClick={(e) => {
e.preventDefault();
goTo("overview");
}}
className={`hidden rounded-full px-3.5 py-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 lg:inline-flex dark:text-slate-300 dark:hover:text-white ${focusRing}`}
>
Sign in
</a>
<a
href="#pricing"
onClick={(e) => {
e.preventDefault();
goTo("pricing");
}}
className={`relative hidden items-center gap-1.5 overflow-hidden rounded-full bg-gradient-to-r from-indigo-600 to-violet-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-transform hover:-translate-y-px active:translate-y-0 sm:inline-flex ${focusRing}`}
>
<span
aria-hidden="true"
className="navxpill-sheen pointer-events-none absolute inset-y-0 -left-full w-1/2 bg-gradient-to-r from-transparent via-white/30 to-transparent"
/>
Start free
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
{/* Mobile toggle */}
<button
ref={toggleRef}
type="button"
onClick={() => setMenuOpen((open) => !open)}
aria-expanded={menuOpen}
aria-controls={panelId}
aria-label={menuOpen ? "Close menu" : "Open menu"}
className={`grid h-9 w-9 place-items-center rounded-full text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 md:hidden dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white ${focusRing}`}
>
{menuOpen ? (
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6 6 18" />
</svg>
) : (
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
)}
</button>
</div>
</nav>
{/* Mobile sheet */}
<AnimatePresence>
{menuOpen && (
<motion.div
id={panelId}
initial={
reduceMotion ? { opacity: 1 } : { opacity: 0, y: -8, scale: 0.98 }
}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={
reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.98 }
}
transition={softTransition}
className="absolute left-0 right-0 top-full z-50 mt-2 origin-top rounded-3xl border border-slate-200/80 bg-white/90 p-3 shadow-xl backdrop-blur-xl md:hidden dark:border-white/10 dark:bg-slate-900/85"
>
<ul className="flex flex-col">
{NAV_ITEMS.map((item, index) => {
const isActive = active === item.id;
return (
<li key={item.id}>
<a
ref={index === 0 ? firstMobileItemRef : undefined}
href={`#${item.id}`}
aria-current={isActive ? "location" : undefined}
onClick={(e) => {
e.preventDefault();
goTo(item.id);
}}
className={[
"flex items-center justify-between rounded-2xl px-4 py-3 text-[15px] font-medium transition-colors",
focusRing,
isActive
? "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-200"
: "text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-white/5",
].join(" ")}
>
{item.label}
{isActive && (
<span
aria-hidden="true"
className="h-1.5 w-1.5 rounded-full bg-indigo-500 dark:bg-indigo-300"
/>
)}
</a>
</li>
);
})}
</ul>
<a
href="#pricing"
onClick={(e) => {
e.preventDefault();
goTo("pricing");
}}
className={`mt-2 flex items-center justify-center gap-1.5 rounded-2xl bg-gradient-to-r from-indigo-600 to-violet-600 px-4 py-3 text-[15px] font-semibold text-white ${focusRing}`}
>
Start free
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Demo content — gives the floating nav something to sit above */}
<div className="pb-24 pt-10">
{SECTIONS.map((section) => (
<section
key={section.id}
id={section.id}
aria-labelledby={`${section.id}-title`}
className="scroll-mt-24 border-b border-slate-200/70 py-16 last:border-0 dark:border-white/10"
>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
{section.eyebrow}
</p>
<h2
id={`${section.id}-title`}
className="mt-3 max-w-2xl text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white"
>
{section.title}
</h2>
<p className="mt-4 max-w-2xl text-base leading-relaxed text-slate-600 dark:text-slate-300">
{section.body}
</p>
{section.id === "features" && (
<div className="mt-8 grid gap-4 sm:grid-cols-3">
{FEATURES.map((f) => (
<div
key={f.title}
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-slate-900/60"
>
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
{f.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{f.copy}
</p>
</div>
))}
</div>
)}
{section.id === "pricing" && (
<div className="mt-8 grid max-w-2xl gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/10 dark:bg-slate-900/60">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
Starter
</p>
<p className="mt-2 text-3xl font-semibold text-slate-900 dark:text-white">
$0
<span className="text-base font-normal text-slate-500 dark:text-slate-400">
{" "}
/ forever
</span>
</p>
<p className="mt-3 text-sm text-slate-600 dark:text-slate-400">
Up to 3 developers and one environment. Everything you need to feel the difference.
</p>
</div>
<div className="rounded-2xl border-2 border-indigo-500 bg-white p-6 dark:bg-slate-900/60">
<p className="text-sm font-medium text-indigo-600 dark:text-indigo-400">
Team
</p>
<p className="mt-2 text-3xl font-semibold text-slate-900 dark:text-white">
$18
<span className="text-base font-normal text-slate-500 dark:text-slate-400">
{" "}
/ active dev
</span>
</p>
<p className="mt-3 text-sm text-slate-600 dark:text-slate-400">
Unlimited environments, 30-day history, and priority on-call routing. Billed monthly.
</p>
</div>
</div>
)}
{section.id === "customers" && (
<figure className="mt-8 max-w-2xl">
<blockquote className="border-l-2 border-indigo-500 pl-4 text-lg font-medium leading-relaxed text-slate-800 dark:text-slate-100">
“We cut our mean-time-to-rollback from eleven minutes to under
forty seconds. The first Friday nobody got paged, I almost
didn't believe it.”
</blockquote>
<figcaption className="mt-3 text-sm text-slate-500 dark:text-slate-400">
Priya Ramanathan — Staff Platform Engineer, Meridian Freight
</figcaption>
<p className="mt-6 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500">
Trusted by teams at
</p>
<div className="mt-3 flex flex-wrap gap-x-6 gap-y-2 text-sm font-medium text-slate-500 dark:text-slate-400">
<span>Meridian</span>
<span>Northwind Labs</span>
<span>Fathom</span>
<span>Quill</span>
<span>Volta</span>
</div>
</figure>
)}
{section.id === "faq" && (
<div className="mt-8 max-w-2xl divide-y divide-slate-200 rounded-2xl border border-slate-200 bg-white dark:divide-white/10 dark:border-white/10 dark:bg-slate-900/60">
{FAQS.map((item) => (
<details key={item.q} className="group px-5 py-4">
<summary
className={`flex cursor-pointer list-none items-center justify-between rounded-md text-sm font-medium text-slate-900 marker:content-none dark:text-white ${focusRing}`}
>
{item.q}
<svg
viewBox="0 0 24 24"
className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-180"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" />
</svg>
</summary>
<p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{item.a}
</p>
</details>
))}
</div>
)}
</section>
))}
<p className="pt-4 text-center text-xs text-slate-400 dark:text-slate-600">
Press <kbd className="rounded border border-slate-300 px-1 font-mono dark:border-white/20">/</kbd> to search · <kbd className="rounded border border-slate-300 px-1 font-mono dark:border-white/20">Esc</kbd> to close
</p>
</div>
</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

