A responsive navigation bar block that sticks to the top of the page. Renders a full-width container with support for a logo, desktop navigation links, and a theme mode toggle. On smaller screens, the menu collapses into a slide-up drawer triggered by a menu button.
Provide menuItems for the navigation links and place your logo in the logo slot.
---
// GENERATED from packages/demos/topbar/default.demo.tsx — edit the source, not this file.
import Topbar from "@pindoba/astro-topbar";
const menuItems = [
{ label: "Home", href: "#" },
{ label: "Docs", href: "#" },
{
label: "Catalog",
items: [
{ label: "Products", href: "#" },
{ label: "Collections", href: "#" },
],
},
{ label: "Blog", href: "#" },
];
---
<Topbar menuItems={menuItems} menuActiveItem="Home" />Use showAppearance to hide the built-in Appearance picker (theme mode, palette, density and corners) — appearanceProps configures both copies, and desktopAppearanceProps / mobileAppearanceProps one side each — menuActiveItem to highlight the current page, and passThrough to override styles on individual slots such as container for layout adjustments.
---
// GENERATED from packages/demos/topbar/custom.demo.tsx — edit the source, not this file.
import Topbar from "@pindoba/astro-topbar";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const menuItems = [
{ label: "Home", href: "#" },
{ label: "Docs", href: "#" },
{ label: "Examples", href: "#" },
];
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
<div>
<h3>Without Theme Mode Select</h3>
<Topbar menuItems={menuItems} showAppearance={false}>
<div
slot="logo"
class={css({
fontSize: "lg",
fontWeight: "bold",
color: "primary.text",
})}
>
MyApp
</div>
</Topbar>
</div>
<div>
<h3>With Active Menu Item</h3>
<Topbar menuItems={menuItems} menuActiveItem="Docs">
<div slot="logo" class={css({ fontSize: "lg", fontWeight: "bold" })}>
Pindoba
</div>
</Topbar>
</div>
<div>
<h3>Custom Background</h3>
<Topbar
menuItems={menuItems}
passThrough={{
root: css.raw({ backgroundColor: "primary.surface.ground" }),
}}
>
<div
slot="logo"
class={css({
fontSize: "lg",
fontWeight: "bold",
color: "primary.text.contrast.bold",
})}
>
Pindoba
</div>
</Topbar>
</div>
</div>Everything in navigationProps reaches both Navigations — the desktop bar and the vertical one inside the mobile drawer — so routing works exactly as in Navigation’s router integration: pass currentHref for the active row and either onNavigate or (React/Vue) a linkComponent.
<Topbar
menuItems={items}
navigationProps={{
currentHref: location.pathname,
onNavigate: (href, event) => {
event.preventDefault();
router.navigate({ to: href });
},
}}
/>
The drawer closes itself after a navigation. A plain click on a link inside the mobile drawer closes it — whether your router handled the click client-side (it ended defaultPrevented) or the browser follows the link natively. The drawer closes only after the click has been dispatched, so your router’s <Link> / onNavigate always receives it first. Cmd/Ctrl/Shift/Alt and middle clicks, target="_blank", download and external links (the same isPlainNavigationClick rule Navigation uses), and submenu toggle buttons leave the drawer open. Focus is not sent back to the hamburger after a navigation — the router or the new page owns it; Escape and the close button still return focus to the trigger as usual.
Controlled drawer. Pass mobileOpen with onMobileOpenChange(open) to own the drawer’s state (the same open / onOpenChange pairing as Dialog). onMobileOpenChange also fires uncontrolled, and reports the automatic close after a navigation as false. In Svelte bind:mobileOpen works; in Vue, v-model:mobileOpen.
const [menuOpen, setMenuOpen] = useState(false);
<Topbar
menuItems={items}
mobileOpen={menuOpen}
onMobileOpenChange={setMenuOpen}
/>;
Astro. There are no client-side props: listen for the bubbling pindoba:navigate event on the topbar to route (the drawer closes the same way), observe the drawer through the pindoba:topbar-mobile-change event (event.detail is { open, reason }, with reason: "navigate" for a close caused by a followed link), and open or close it with setTopbarMobileOpen:
import {
setTopbarMobileOpen,
TOPBAR_MOBILE_CHANGE_EVENT,
} from "@pindoba/astro-topbar/runtime";
const topbar = document.querySelector("[data-component='topbar']");
topbar.addEventListener(TOPBAR_MOBILE_CHANGE_EVENT, (event) => {
console.log(event.detail.open, event.detail.reason);
});
setTopbarMobileOpen(topbar, false);
The demo runs a tiny in-memory router. Narrow the window, open the menu and pick an item: the route changes without a page load, the current item follows, and the drawer closes.
Route: ?route=inbox · client-side navigations: 0
---
// GENERATED from packages/demos/topbar/router-links.demo.tsx — edit the source, not this file.
import Topbar from "@pindoba/astro-topbar";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const menuItems = [
{ label: "Inbox", href: "?route=inbox" },
{ label: "Drafts", href: "?route=drafts" },
{
label: "Settings",
items: [
{ label: "Profile", href: "?route=settings/profile" },
{ label: "Billing", href: "?route=settings/billing" },
],
},
];
const status = css({ fontSize: "sm", color: "neutral.text.muted" });
const path = "?route=inbox";
const navigations = 0;
---
<div class={stack({ gap: "md" })} data-ir-root="topbar/router-links">
<Topbar
menuItems={menuItems}
navigationProps={{
currentHref: path,
}}
data-ir="1"
/>
<p class={status} data-router-status>
Route:{" "}
<code>
<!--ir:2-->
{path}
<!--/ir-->
</code>{" "}
· client-side navigations: <!--ir:3-->
{navigations}
<!--/ir-->
</p>
</div>
<script>
import { hydrate } from "./_client/router-links.ts";
document
.querySelectorAll('[data-ir-root="topbar/router-links"]')
.forEach((root) => hydrate([root]));
</script>