Skip to content
LofiStack Gallery
Type: navbarWeek 04

Dynamic Island Nav

A dark floating pill for site navigation. A soft highlight slides to the active link. Hover or open a link that has a sub-menu and the island grows downward into a mega-menu. Tap search and it grows into a search box with suggestions. Push a notification and it pops and expands to show the alert, then shrinks back when cleared. The height animates with a CSS grid trick, so there's no measuring and no layout jank.

View on GitHub

home

We design and build calm, fast websites.

  • Hover “Work”, click the search icon, or send a notification.

Usage

usage.tsx
import { DynamicIslandNav } from "@/components/gallery/dynamic-island-nav";

<DynamicIslandNav
  logo={<Logo />}
  activeId={page}
  onNavigate={setPage}
  onSearch={(q) => router.push(`/search?q=${q}`)}
  links={[
    { id: "home", label: "Home", href: "/" },
    {
      id: "work",
      label: "Work",
      menu: [
        { label: "Case studies", description: "Deep dives", href: "/work", icon: <BookIcon /> },
        { label: "Clients", href: "/clients", icon: <UserIcon /> },
      ],
    },
    { id: "blog", label: "Blog", href: "/blog" },
  ]}
  notification={toast}               // { title, message?, icon? } or null
  onDismissNotification={() => setToast(null)}
/>

States

Resting pill · Active highlight sliding · Hover / focus on links · Menu expanded (mega-menu) · Search expanded with suggestions · Notification pop + expand · Collapsing back · Narrow screens (links scroll inside the pill)

Default · logo + search
Active link on Blog
Notification expanded
Plain links (no menus)

Props

  • links*

    { id; label; href?; menu?: { label; description?; href?; icon? }[] }[]

    Top-level links. A link with a menu becomes a button that expands the island.

  • activeId / defaultActiveId

    string

    Controlled or initial active link. The highlight slides to it.

  • onNavigate

    (id) => void

    Called when a link or menu item is chosen.

  • logo

    ReactNode

    Mark in the circle on the left.

  • label= "Main"

    string

    Accessible name of the <nav> landmark.

  • onSearch

    (query) => void

    Adds the search button. Called on Enter or when a suggestion is clicked.

  • notification

    { title; message?; icon? } | null

    When set, the island pops and expands to show it. Set null to collapse.

  • onDismissNotification

    () => void

    Adds a dismiss button to the notification.

  • className

    string

    Extra classes for the nav wrapper.

Accessibility

  • A <nav> landmark with a label. The active link has aria-current="page".
  • Menu and search triggers are buttons with aria-expanded and aria-controls pointing at the panel.
  • ArrowDown on a menu trigger opens it and moves focus to the first item. Esc closes it and returns focus to the trigger.
  • The collapsed panel is inert, so hidden links can't be tabbed to.
  • Notifications are announced through a polite live region, and the dismiss button has a label.
  • All morphing and sliding motion is disabled under prefers-reduced-motion.

Build prompt

The final prompt used to build this component
Build a reusable React 19 + TypeScript + Tailwind v4 navbar called DynamicIslandNav: a floating dark pill that morphs like the iPhone Dynamic Island.

Props (typed): links ({ id, label, href?, menu?: { label, description?, href?, icon? }[] }[]), activeId / defaultActiveId, onNavigate, logo, label, onSearch, notification ({ title, message?, icon? } | null), onDismissNotification, className.

Look: an inline pill (bg-zinc-950, white/10 ring, big soft shadow, rounded 28px) with a round logo slot, a row of links and a round search button. A white/12 highlight sits behind the active link and slides (left/width transition) to the new one. Measure tab positions with a ResizeObserver on the row (setState in the observer callback, not in the effect body).

Morph: under the row, a panel animates height with grid-template-rows 0fr → 1fr (no JS measuring). It shows one of:
- menu: a 2-column mega-menu of icon + label + description links (opens on mouse hover or click, closes on pointer-leave after 140ms, outside click or Esc)
- search: a rounded input with ↵ hint and suggestion chips matched from link and menu labels. Enter calls onSearch.
- notification: icon, title, message and a dismiss ×. The pill does a quick scale "pop" when it arrives.
Keep the last panel content in state while collapsing so it doesn't vanish mid-animation. Track the last notification with the "adjust state during render" pattern. Panel children fade in on change.

Accessibility: <nav aria-label>, aria-current on the active link, triggers with aria-expanded and aria-controls, ArrowDown opens a menu and focuses its first item, Esc closes and restores focus, the collapsed panel is inert, a polite live region for notifications, focus-visible rings, and reduced-motion support. No dependencies.