Morph Navbar
stableA navbar whose menus share one floating box. Moving from menu to menu, the box glides to the new menu and changes size to fit it, while the links slide across.
Built from Accordion, Button, and Glide Highlight.
Preview
menus without pictures, and the current page marked
narrow container: press the menu button
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the website component. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/morph-navbar
Use it for
- The top of a product site with three to five menus, where people move along the bar to compare them.
- Give each item a short `description` when its name alone does not say enough.
- Give each item in a menu an `icon` to help people scan it. Give every item in that menu one, or none.
- Give each item in one menu a `preview`, a small picture drawn in code, to show the page before people open it. Hovering or focusing an item switches the picture.
- Mark the page you are on with `current: true`.
- Pass your router's link through `renderLink`, like next/link, so links stay client-side.
- Put a quiet button and a main call to action in `actions`. On narrow screens they move to the end of the menu.
- Set `collapseBelow` to the smallest width your links and actions fit in.
Avoid it for
- One or two plain menus. Use Navbar, whose menus open small lists under each link.
- Pictures in more than one menu. One menu with pictures draws the eye. More of them crowd the bar.
- More than about eight items in one menu. Split it, or link to a page that lists them all.
- A menu inside a menu. Each menu is one level of links, and on narrow screens one level of sections.
- Links inside an app's own screens. Use Tabs, or a sidebar.
Props
- brand
Your logo or name. It links to brandHref.
ReactNode
- brandHref
Where the brand links.
string · default "/"
- links
The main links, in order. Each href and each menu label must be unique, since they are keys.
(MorphNavbarLink | MorphNavbarMenu)[]
- MorphNavbarLink
Type. A link in the bar. current marks the page you are on, with aria-current="page".
{ href: string; label: string; current?: boolean }
- MorphNavbarMenu
Type. A menu in the bar. It opens on hover after a short pause, or on a click or Enter. An item's description shows under its label, and its icon before the label, in the muted color until the row is hovered or focused. When any item has a preview, the menu shows the picture of the row under the pointer or focus beside the list.
{ label: string; items: { href: string; label: string; description?: ReactNode; icon?: ReactNode; preview?: ReactNode; current?: boolean }[] }
- actions
Buttons on the right, like Talk to sales and Create an account. On narrow screens they sit at the end of the menu.
ReactNode
- renderLink
Draws the brand and each link. Spread every prop onto your router's link: (props) => <Link {...props} />. It is a function, so pass it from a client component, not a server page.
(props: MorphNavbarLinkProps) => ReactNode · default a plain <a>
- label
The name screen readers give the nav.
string · default "Main"
- menuLabel
The name of the menu button on narrow screens.
string · default "Menu"
- collapseBelow
The navbar's own width below which links fold into the menu: 36, 42, 48, or 56rem. Pick the smallest your links and actions fit in.
"sm" | "md" | "lg" | "xl" · default "lg"
- className
Extra classes on the header, which holds the container query. Add sticky top-0 z-40 here to pin it.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| brand | ReactNode | - | Your logo or name. It links to brandHref. |
| brandHref | string | "/" | Where the brand links. |
| links | (MorphNavbarLink | MorphNavbarMenu)[] | - | The main links, in order. Each href and each menu label must be unique, since they are keys. |
| MorphNavbarLink | { href: string; label: string; current?: boolean } | - | Type. A link in the bar. current marks the page you are on, with aria-current="page". |
| MorphNavbarMenu | { label: string; items: { href: string; label: string; description?: ReactNode; icon?: ReactNode; preview?: ReactNode; current?: boolean }[] } | - | Type. A menu in the bar. It opens on hover after a short pause, or on a click or Enter. An item's description shows under its label, and its icon before the label, in the muted color until the row is hovered or focused. When any item has a preview, the menu shows the picture of the row under the pointer or focus beside the list. |
| actions | ReactNode | - | Buttons on the right, like Talk to sales and Create an account. On narrow screens they sit at the end of the menu. |
| renderLink | (props: MorphNavbarLinkProps) => ReactNode | a plain <a> | Draws the brand and each link. Spread every prop onto your router's link: (props) => <Link {...props} />. It is a function, so pass it from a client component, not a server page. |
| label | string | "Main" | The name screen readers give the nav. |
| menuLabel | string | "Menu" | The name of the menu button on narrow screens. |
| collapseBelow | "sm" | "md" | "lg" | "xl" | "lg" | The navbar's own width below which links fold into the menu: 36, 42, 48, or 56rem. Pick the smallest your links and actions fit in. |
| className | string | - | Extra classes on the header, which holds the container query. Add sticky top-0 z-40 here to pin it. |
Accessibility
- The links sit in a nav landmark named by `label`. Only one copy of the links is on screen at a time, so screen readers hear one nav.
- A menu's trigger is a button with aria-expanded. Enter, Space, Arrow Down, or a click opens it. Tab moves into the open menu, and the left and right arrow keys move along the bar.
- Hover opens a menu too, but never only hover.
- Escape closes the menu and puts focus back on its trigger. Picking a link, by click or Enter, closes it too.
- The current link has aria-current="page".
- Pictures and icons are hidden from screen readers. Each link's label and description say where it goes.
- With reduced motion the menus fade from one to the next without sliding.
- On narrow screens a menu button opens the links, and says whether it is open with aria-expanded. Each menu is a section that opens in place. Escape or a press outside closes it.
- Links in the narrow menu are at least 44px tall for touch.
- On narrow screens `actions` render a second time, inside the menu. Do not give them an id.
Examples
Menus that share one box
<MorphNavbar
brand="Workspace"
links={[
{
label: "Product",
items: [
{ href: "/chat", label: "Chat", description: "Ask questions and get answers from your files." },
{ href: "/docs", label: "Docs", description: "Write, share, and comment on pages together." },
],
},
{
label: "Company",
items: [
{ href: "/about", label: "About" },
{ href: "/careers", label: "Careers" },
],
},
{ href: "/pricing", label: "Pricing" },
]}
actions={<Button size="sm">Create an account</Button>}
/>A menu that shows a picture of each page
<MorphNavbar
brand="Workspace"
links={[
{
label: "Product",
items: [
{ href: "/chat", label: "Chat", description: "Ask questions.", preview: <ChatPicture /> },
{ href: "/boards", label: "Boards", description: "Track work in columns.", preview: <BoardsPicture /> },
],
},
]}
/>A menu with an icon on each item
<MorphNavbar
brand="Workspace"
links={[
{
label: "Developers",
items: [
{ href: "/api", label: "API", icon: <Braces />, description: "Build on your workspace's data." },
{ href: "/guides", label: "Guides", icon: <BookOpen />, description: "Step-by-step help for common tasks." },
{ href: "/status", label: "Status", icon: <Activity />, description: "Uptime and incidents, live." },
],
},
]}
/>With next/link, pinned to the top
<MorphNavbar
className="sticky top-0 z-40 border-b"
brand="Workspace"
links={[{ href: "/", label: "Home", current: true }, { href: "/blog", label: "Blog" }]}
renderLink={(props) => <Link {...props} />}
/>