# Nav Local

> A sticky product sub-navigation that highlights the section in view, shown on a dark product page.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/nav-local).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/nav-local
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/nav-local-light-36b67511120c76e5.png
- Registry item: https://suiss.app/ui/r/new-york-v4/nav-local.json
- Categories: blocks, nav
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/nav-local.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/button`, `@esuiss/navbar`

## Files

### `components/blocks/nav-local.tsx`

```tsx
"use client"

import * as React from "react"

import { Navbar } from "@/components/patterns/navbar"
import { Button } from "@/components/ui/button"

const images = {
  hero: "https://images.unsplash.com/photo-1714972384975-fbd4f19b1bb3?w=1600&q=80&auto=format&fit=crop",
  detail:
    "https://images.unsplash.com/photo-1695083691065-4f77dfd0f0d5?w=1600&q=80&auto=format&fit=crop",
}

type Section = { id: string; label: string }

const sections: [Section, ...Section[]] = [
  { id: "overview", label: "Overview" },
  { id: "why", label: "Why suiss" },
  { id: "specs", label: "Tech Specs" },
]

const highlights = [
  { value: "S19 Pro", text: "The fastest chip ever in a phone." },
  { value: "48 MP", text: "Fusion resolution across all three cameras." },
  { value: "33 hours", text: "The longest video playback yet." },
]

export function NavLocal() {
  const [scrolled, setScrolled] = React.useState(false)
  const [active, setActive] = React.useState(sections[0].id)
  const topRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    const top = topRef.current
    if (!top) return
    const observer = new IntersectionObserver(([entry]) =>
      setScrolled(entry ? !entry.isIntersecting : false)
    )
    observer.observe(top)
    return () => observer.disconnect()
  }, [])

  React.useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const visible = entries.find((entry) => entry.isIntersecting)
        if (visible) setActive(visible.target.id)
      },
      { rootMargin: "-50% 0px -50% 0px" }
    )
    for (const section of sections) {
      const element = document.getElementById(section.id)
      if (element) observer.observe(element)
    }
    return () => observer.disconnect()
  }, [])

  return (
    <div className="dark bg-background text-foreground">
      <div
        ref={topRef}
        aria-hidden
        className="h-(--nav-local-threshold) -mb-(--nav-local-threshold) [--nav-local-threshold:8px]"
      />
      <Navbar
        data-scrolled={scrolled ? "" : undefined}
        items={sections.map((section) => ({
          label: section.label,
          href: `#${section.id}`,
          active: section.id === active,
        }))}
        brand={
          <span className="text-xl font-semibold tracking-tight">
            Phone Pro
          </span>
        }
        actions={
          <Button size="sm" className="h-7 px-3 text-xs">
            Buy
          </Button>
        }
        className="sticky top-0 z-10 h-13 border-b border-foreground/18 transition-[background-color,border-color] duration-300 data-scrolled:border-foreground/12 data-scrolled:bg-muted/80 data-scrolled:backdrop-blur-xl data-scrolled:backdrop-saturate-180 md:px-[max(--spacing(5),calc((100%-62.5rem)/2))] [&_[data-slot=navigation-menu]]:me-0 [&_[data-slot=navigation-menu]]:ms-auto [&_[data-slot=navigation-menu-link]]:relative [&_[data-slot=navigation-menu-link]]:h-13 [&_[data-slot=navigation-menu-link]]:rounded-none [&_[data-slot=navigation-menu-link]]:px-3 [&_[data-slot=navigation-menu-link]]:text-xs [&_[data-slot=navigation-menu-link]]:text-muted-foreground [&_[data-slot=navigation-menu-link]]:hover:bg-transparent [&_[data-slot=navigation-menu-link]]:hover:text-foreground [&_[data-slot=navigation-menu-link]]:data-active:bg-transparent [&_[data-slot=navigation-menu-link]]:data-active:text-foreground [&_[data-slot=navigation-menu-link]]:data-active:after:absolute [&_[data-slot=navigation-menu-link]]:data-active:after:inset-x-3 [&_[data-slot=navigation-menu-link]]:data-active:after:-bottom-px [&_[data-slot=navigation-menu-link]]:data-active:after:h-px [&_[data-slot=navigation-menu-link]]:data-active:after:bg-foreground"
      />
      <section
        id="overview"
        className="flex flex-col items-center px-6 pt-14 pb-6 text-center"
      >
        <p className="mb-2 text-sm text-muted-foreground">New</p>
        <h1 className="text-5xl font-semibold tracking-tight text-balance md:text-6xl">
          Pro power.
        </h1>
        <p className="mt-2 text-2xl text-balance text-muted-foreground">
          The most advanced phone yet.
        </p>
      </section>
      <img
        src={images.hero}
        alt="An orange phone on a gradient background"
        className="mx-auto h-90 w-[calc(100%-2.5rem)] max-w-5xl rounded-[1.375rem] bg-secondary object-cover md:w-[calc(100%-5rem)]"
      />
      <section
        id="why"
        className="mx-auto grid max-w-5xl gap-3.5 px-5 py-10 sm:grid-cols-3 md:px-10"
      >
        {highlights.map((item) => (
          <div key={item.value} className="rounded-[1.125rem] bg-muted p-5.5">
            <p className="text-3xl font-semibold tabular-nums">{item.value}</p>
            <p className="mt-1.5 text-base text-muted-foreground">
              {item.text}
            </p>
          </div>
        ))}
      </section>
      <section id="specs" className="px-5 pb-10 md:px-10">
        <img
          src={images.detail}
          alt="A close look at a camera lens"
          className="mx-auto h-80 w-full max-w-5xl rounded-[1.375rem] bg-secondary object-cover"
        />
      </section>
    </div>
  )
}
```
