A minimal top bar with the brand and a button, plus a column of section dots fixed at the right edge whose labels slide out on hover or focus; an IntersectionObserver moves the active dot as each section crosses the middle of the screen. On a phone the dots sit in a bottom row with the current section name, which opens a numbered section list above it. The list stays in the markup and is toggled with CSS.
A minimal sticky bar with the brand and one button, paired with a column of section dots fixed at the right edge of the screen. Hovering or focusing a dot slides its label out, clicking scrolls to that section, and the active dot follows the scroll on its own. On a phone the dots move into a floating row at the bottom with the current section name, which opens a numbered list of every section above it. The block renders the demo sections the dots observe.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.
Radix flavor
npx shadcn add "https://beste.dev/r/navbar114?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://beste.dev/r-base/navbar114?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/navbar114.tsx plus the shadcn button primitive it uses.
The installed file exports navbar114Demo alongside the block: the exact props behind the preview above. Spread it to get a working navbar in one line.
import { Navbar114, navbar114Demo } from "@/components/beste/block/navbar114";
export default function Page() {
return <Navbar114 {...navbar114Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Navbar114 } from "@/components/beste/block/navbar114";
export default function Page() {
return (
<Navbar114
brand="Meridian"
sections={[
{ label: "Overview", heading: "One calm place for every project", description: "Plans, files and decisions in one timeline." },
{ label: "Planning", heading: "Plans that stay honest", description: "Milestones move when the work moves." },
{ label: "Pricing", heading: "Free for teams of five", description: "One price per active seat." },
]}
actions={[{ label: "Start free", href: "/signup" }]}
labels={{ sections: "Page sections", open: "Show all sections", close: "Hide sections", goTo: "Go to" }}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
brand | string | – | Wordmark on the left of the bar |
sections | Section[] | [] | Page sections rendered under the bar; each one gets a dot |
actions | ActionButton[] | [] | Buttons on the right of the bar |
labels | { sections?: string; open?: string; close?: string; goTo?: string } | {} | Name of the dot navigation, names for the mobile list toggle and the prefix read before each dot label |
className | string | – | Extra classes for the outer wrapper |
type Section = {
label: string;
heading: string;
description: string;
image?: { src: string; alt: string };
};
type ActionButton = {
label: string;
href: string;
variant?: "default" | "outline" | "ghost" | "secondary";
};IntersectionObserver with a -50% 0px -50% 0px root margin watches every section, so the active dot changes exactly when a section crosses the middle of the viewport.scrollIntoView, smooth unless the visitor prefers reduced motion; sections carry scroll-mt-16 so the sticky bar never covers a heading.aria-current when its section is in view.aria-hidden and inert while closed; the toggle shows the current section name with aria-expanded, and Escape closes it.