Navbar
A horizontal bar of links and menus for the top of an app or a site.
import { Avatar } from '@/components/avatar'
import {
Dropdown,
DropdownButton,
DropdownDivider,
DropdownItem,
DropdownLabel,
DropdownMenu,
} from '@/components/dropdown'
import {
Navbar,
NavbarDivider,
NavbarItem,
NavbarLabel,
NavbarSection,
NavbarSpacer,
} from '@/components/navbar'
import {
ArrowRightStartOnRectangleIcon,
BookOpenIcon,
ChevronDownIcon,
Cog6ToothIcon,
LockClosedIcon,
PlusIcon,
UserCircleIcon,
} from '@heroicons/react/16/solid'
import { BellIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'
export default function Example() {
return (
<Navbar>
<Dropdown>
<DropdownButton as={NavbarItem} aria-label="Switch workspace">
<Avatar initials="NW" className="bg-teal-600 text-white" />
<NavbarLabel>Northwind</NavbarLabel>
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu className="min-w-64" anchor="bottom start">
<DropdownItem href="/settings/workspace">
<Cog6ToothIcon />
<DropdownLabel>Workspace settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/w/northwind">
<Avatar initials="NW" className="bg-teal-600 text-white" />
<DropdownLabel>Northwind</DropdownLabel>
</DropdownItem>
<DropdownItem href="/w/harmattan-freight">
<Avatar initials="HF" className="bg-amber-500 text-white" />
<DropdownLabel>Harmattan Freight</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/workspaces/new">
<PlusIcon />
<DropdownLabel>Create workspace</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
<NavbarDivider className="max-lg:hidden" />
<NavbarSection className="max-lg:hidden">
<NavbarItem href="/overview" current>
Overview
</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
<Dropdown>
<DropdownButton as={NavbarItem} aria-label="Account">
<Avatar src="/users/avatar.jpg" square />
</DropdownButton>
<DropdownMenu className="min-w-56" anchor="bottom end">
<DropdownItem href="/profile">
<UserCircleIcon />
<DropdownLabel>Your profile</DropdownLabel>
</DropdownItem>
<DropdownItem href="/profile/security">
<LockClosedIcon />
<DropdownLabel>Security</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/docs/api">
<BookOpenIcon />
<DropdownLabel>API reference</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/logout">
<ArrowRightStartOnRectangleIcon />
<DropdownLabel>Sign out</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarSection>
</Navbar>
)
}<script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Dropdown from '@/components/dropdown.vue'
import DropdownButton from '@/components/dropdown-button.vue'
import DropdownDivider from '@/components/dropdown-divider.vue'
import DropdownItem from '@/components/dropdown-item.vue'
import DropdownLabel from '@/components/dropdown-label.vue'
import DropdownMenu from '@/components/dropdown-menu.vue'
import Navbar from '@/components/navbar.vue'
import NavbarDivider from '@/components/navbar-divider.vue'
import NavbarItem from '@/components/navbar-item.vue'
import NavbarLabel from '@/components/navbar-label.vue'
import NavbarSection from '@/components/navbar-section.vue'
import NavbarSpacer from '@/components/navbar-spacer.vue'
import {
ArrowRightStartOnRectangleIcon,
BookOpenIcon,
ChevronDownIcon,
Cog6ToothIcon,
LockClosedIcon,
PlusIcon,
UserCircleIcon,
} from '@heroicons/vue/16/solid'
import { BellIcon, MagnifyingGlassIcon } from '@heroicons/vue/20/solid'
</script>
<template>
<Navbar>
<Dropdown>
<DropdownButton :as="NavbarItem" aria-label="Switch workspace">
<Avatar initials="NW" class="bg-teal-600 text-white" />
<NavbarLabel>Northwind</NavbarLabel>
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu class="min-w-64" anchor="bottom start">
<DropdownItem href="/settings/workspace">
<Cog6ToothIcon />
<DropdownLabel>Workspace settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/w/northwind">
<Avatar initials="NW" class="bg-teal-600 text-white" />
<DropdownLabel>Northwind</DropdownLabel>
</DropdownItem>
<DropdownItem href="/w/harmattan-freight">
<Avatar initials="HF" class="bg-amber-500 text-white" />
<DropdownLabel>Harmattan Freight</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/workspaces/new">
<PlusIcon />
<DropdownLabel>Create workspace</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
<NavbarDivider class="max-lg:hidden" />
<NavbarSection class="max-lg:hidden">
<NavbarItem href="/overview" current>Overview</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
<Dropdown>
<DropdownButton :as="NavbarItem" aria-label="Account">
<Avatar src="/users/avatar.jpg" square />
</DropdownButton>
<DropdownMenu class="min-w-56" anchor="bottom end">
<DropdownItem href="/profile">
<UserCircleIcon />
<DropdownLabel>Your profile</DropdownLabel>
</DropdownItem>
<DropdownItem href="/profile/security">
<LockClosedIcon />
<DropdownLabel>Security</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/docs/api">
<BookOpenIcon />
<DropdownLabel>API reference</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/logout">
<ArrowRightStartOnRectangleIcon />
<DropdownLabel>Sign out</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarSection>
</Navbar>
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { LayoutGroup, motion } from 'motion/react'
import React, { forwardRef, useId } from 'react'
import { TouchTarget } from './button'
import { Link } from './link'
export function Navbar({ className, ...props }: React.ComponentPropsWithoutRef<'nav'>) {
return <nav aria-label="Main" {...props} className={clsx(className, 'flex flex-1 items-center gap-4 py-2.5')} />
}
export function NavbarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div aria-hidden="true" {...props} className={clsx(className, 'w-px h-6 bg-zinc-950/10 dark:bg-white/10')} />
}
export function NavbarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
let id = useId()
return (
<LayoutGroup id={id}>
<div {...props} className={clsx(className, 'flex items-center gap-3')} />
</LayoutGroup>
)
}
export function NavbarSpacer({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div aria-hidden="true" {...props} className={clsx(className, '-ml-4 flex-1')} />
}
const item = {
layout: 'relative flex min-w-0 items-center gap-3 p-2 rounded-lg',
text: 'text-left text-base/6 sm:text-sm/5 text-zinc-950 dark:text-white font-medium',
background: 'data-hover:bg-zinc-950/5 dark:data-hover:bg-white/5 data-active:bg-zinc-950/5 dark:data-active:bg-white/5',
avatar: [
'*:data-[slot=avatar]:size-7 sm:*:data-[slot=avatar]:size-6 *:data-[slot=avatar]:-m-0.5',
'*:data-[slot=avatar]:[--avatar-radius:var(--radius-md)]',
],
}
const itemIcon = {
size: '*:data-[slot=icon]:shrink-0 *:data-[slot=icon]:size-6 sm:*:data-[slot=icon]:size-5',
trailing: [
'*:not-nth-2:last:data-[slot=icon]:size-5 sm:*:not-nth-2:last:data-[slot=icon]:size-4',
'*:not-nth-2:last:data-[slot=icon]:ml-auto',
],
fill: [
'*:data-[slot=icon]:fill-zinc-500 dark:*:data-[slot=icon]:fill-zinc-400',
'data-hover:*:data-[slot=icon]:fill-zinc-950 dark:data-hover:*:data-[slot=icon]:fill-white',
'data-active:*:data-[slot=icon]:fill-zinc-950 dark:data-active:*:data-[slot=icon]:fill-white',
],
}
const itemClasses = clsx(Object.values(item), Object.values(itemIcon))
const currentIndicator = 'absolute -bottom-2.5 inset-x-2 h-0.5 rounded-full bg-zinc-950 dark:bg-white'
type NavbarItemProps = {
current?: boolean
className?: string
children: React.ReactNode
} & (
| ({ href?: never } & Omit<Headless.ButtonProps, 'as' | 'className'>)
| ({ href: string } & Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>)
)
export const NavbarItem = forwardRef(function NavbarItem(
{ current, className, children, ...props }: NavbarItemProps,
ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement>
) {
let dataCurrent = current ? ('true' as const) : undefined
return (
<span className={clsx(className, 'relative')}>
{current && (
<motion.span layoutId="current-indicator" className={currentIndicator} />
)}
{typeof props.href === 'string' ? (
<Link
{...props}
className={itemClasses}
data-current={dataCurrent}
aria-current={current ? 'page' : undefined}
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
>
<TouchTarget>{children}</TouchTarget>
</Link>
) : (
<Headless.Button
{...props}
className={clsx('cursor-default', itemClasses)}
data-current={dataCurrent}
aria-current={dataCurrent}
ref={ref}
>
<TouchTarget>{children}</TouchTarget>
</Headless.Button>
)}
</span>
)
})
export function NavbarLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
return <span {...props} className={clsx(className, 'truncate')} />
}Also copy Button.
<template>
<nav aria-label="Main" class="flex flex-1 items-center gap-4 py-2.5">
<slot />
</nav>
</template>Component API
| Prop | Default | Description |
|---|---|---|
Navbarrenders a <nav> element | ||
aria-label | Main | Name of the navigation landmark. Change it when the page holds another `nav`, or to match your app's language. |
NavbarSectionrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
NavbarItemrenders a <button> element | ||
href | - | Turns the item into a link rendered by your router. Without it, the item is a button. |
current | false | Marks the page the user is on and draws the underline. |
disabled | false | Blocks a button item. |
NavbarLabelrenders a <span> element | ||
| This component does not expose any component-specific props. | ||
NavbarSpacerrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
NavbarDividerrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
| Prop | Default | Description |
|---|---|---|
Navbarrenders a <nav> element | ||
aria-label | Main | Name of the navigation landmark. Change it when the page holds another `nav`, or to match your app's language. |
NavbarSectionrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
NavbarItemrenders a <button> element | ||
href | - | Turns the item into a link rendered by your router. Without it, the item is a button. |
current | false | Marks the page the user is on and draws the underline. |
disabled | false | Blocks a button item. |
NavbarLabelrenders a <span> element | ||
| This component does not expose any component-specific props. | ||
NavbarSpacerrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
NavbarDividerrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
Usage
A top bar has room for four or five destinations before it starts to crowd. Put the areas people open every day there, and move the rest into a menu or a sidebar.
Link the main areas of your app
Wrap NavbarItem elements in a NavbarSection inside the Navbar. An item with href is a link; leave href off and it becomes a button for actions that stay on the page:
import { Navbar, NavbarItem, NavbarSection } from '@/components/navbar'
export default function Example() {
return (
<Navbar>
<NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
</Navbar>
)
}<script setup lang="ts">
import Navbar from '@/components/navbar.vue'
import NavbarItem from '@/components/navbar-item.vue'
import NavbarSection from '@/components/navbar-section.vue'
</script>
<template>
<Navbar>
<NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
</Navbar>
</template>Show where the user is
Pass current to the item of the open page. It gets an underline, and when current moves to another item of the same section the underline slides over to it:
<NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/payments" current>
Payments
</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection><NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/payments" current>Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>Structure
Navbar is a flex row. Sections, a spacer and dividers decide how the pieces sit against each other, and any element you drop in, such as a logo, takes its place in the row.
Start with your logo
Put a logo link first so the brand doubles as a way back to the home page:
<Navbar>
<Link href="/overview" aria-label="Overview">
<Logo className="size-10 sm:size-8" />
</Link>
<NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/invoices" current>
Invoices
</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
</Navbar><Navbar>
<Link href="/overview" aria-label="Overview">
<Logo class="size-10 sm:size-8" />
</Link>
<NavbarSection>
<NavbarItem href="/overview">Overview</NavbarItem>
<NavbarItem href="/invoices" current>Invoices</NavbarItem>
<NavbarItem href="/customers">Customers</NavbarItem>
</NavbarSection>
</Navbar>Push utilities to the right
NavbarSpacer takes all the free width, so whatever follows it lands against the far edge. Keep page links on the left and tools like help or notifications on the right:
<Navbar>
<NavbarSection>
<NavbarItem href="/overview" current>
Overview
</NavbarItem>
<NavbarItem href="/payouts">Payouts</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/help" aria-label="Help">
<QuestionMarkCircleIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
</NavbarSection>
</Navbar><Navbar>
<NavbarSection>
<NavbarItem href="/overview" current>Overview</NavbarItem>
<NavbarItem href="/payouts">Payouts</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/help" aria-label="Help">
<QuestionMarkCircleIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
</NavbarSection>
</Navbar>Separate groups of links
A NavbarDivider draws a short vertical rule. Use it where the links on each side answer different needs, like money movement and developer tools:
<Navbar>
<Link href="/overview" aria-label="Overview">
<Logo className="size-10 sm:size-8" />
</Link>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/payouts">Payouts</NavbarItem>
</NavbarSection>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/developers">Developers</NavbarItem>
</NavbarSection>
</Navbar><Navbar>
<Link href="/overview" aria-label="Overview">
<Logo class="size-10 sm:size-8" />
</Link>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/payouts">Payouts</NavbarItem>
</NavbarSection>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/developers">Developers</NavbarItem>
</NavbarSection>
</Navbar>Composition
Menus open from the bar by rendering a DropdownButton as a NavbarItem. The trigger then shares the hover state, spacing and icon sizing of the links next to it. The Dropdown page covers what the menu itself can hold.
Switch between workspaces
An avatar, a NavbarLabel and a trailing chevron make the current workspace readable at a glance. The label truncates when space runs out:
<Navbar>
<Dropdown>
<DropdownButton as={NavbarItem} aria-label="Switch workspace">
<Avatar initials="NW" className="bg-teal-600 text-white" />
<NavbarLabel>Northwind</NavbarLabel>
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu className="min-w-64" anchor="bottom start">
{/* ... */}
</DropdownMenu>
</Dropdown>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/payouts" current>
Payouts
</NavbarItem>
<NavbarItem href="/developers">Developers</NavbarItem>
</NavbarSection>
</Navbar><Navbar>
<Dropdown>
<DropdownButton :as="NavbarItem" aria-label="Switch workspace">
<Avatar initials="NW" class="bg-teal-600 text-white" />
<NavbarLabel>Northwind</NavbarLabel>
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu class="min-w-64" anchor="bottom start">
<!-- ... -->
</DropdownMenu>
</Dropdown>
<NavbarDivider />
<NavbarSection>
<NavbarItem href="/payouts" current>Payouts</NavbarItem>
<NavbarItem href="/developers">Developers</NavbarItem>
</NavbarSection>
</Navbar>Open the account menu from an avatar
An avatar on its own is enough for the account menu. Anchor it at bottom end so the menu opens toward the page instead of off screen:
<Navbar>
<Link href="/overview" aria-label="Overview">
<Logo className="size-10 sm:size-8" />
</Link>
<NavbarSpacer />
<Dropdown>
<DropdownButton as={NavbarItem} aria-label="Account">
<Avatar src="/users/avatar.jpg" square />
</DropdownButton>
<DropdownMenu className="min-w-56" anchor="bottom end">
{/* ... */}
</DropdownMenu>
</Dropdown>
</Navbar><Navbar>
<Link href="/overview" aria-label="Overview">
<Logo class="size-10 sm:size-8" />
</Link>
<NavbarSpacer />
<Dropdown>
<DropdownButton :as="NavbarItem" aria-label="Account">
<Avatar src="/users/avatar.jpg" square />
</DropdownButton>
<DropdownMenu class="min-w-56" anchor="bottom end">
<!-- ... -->
</DropdownMenu>
</Dropdown>
</Navbar>Responsive behavior
The navbar sets no breakpoints of its own. You choose what stays visible on each screen size with utility classes. For page links that scroll sideways on small screens, put them in the tabs row of StackedLayout.
Hide the links on narrow screens
Add max-lg:hidden to the section of page links and to the divider before it. The logo and the tools on the right stay in place:
<Navbar>
<Link href="/overview" aria-label="Overview">
<Logo className="size-10 sm:size-8" />
</Link>
<NavbarDivider className="max-lg:hidden" />
<NavbarSection className="max-lg:hidden">
<NavbarItem href="/overview" current>
Overview
</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
</NavbarSection>
</Navbar><Navbar>
<Link href="/overview" aria-label="Overview">
<Logo class="size-10 sm:size-8" />
</Link>
<NavbarDivider class="max-lg:hidden" />
<NavbarSection class="max-lg:hidden">
<NavbarItem href="/overview" current>Overview</NavbarItem>
<NavbarItem href="/payments">Payments</NavbarItem>
<NavbarItem href="/invoices">Invoices</NavbarItem>
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
</NavbarSection>
</Navbar>Accessibility
The bar renders a nav element. A current link carries aria-current="page", and NavbarSpacer and NavbarDivider are hidden from assistive technology.
Name icon-only items
An icon has no text for a screen reader to announce, so give every icon-only item an aria-label. Icons that do not come from Heroicons need data-slot="icon" to get the same size and color:
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem href="/help" aria-label="Help">
<QuestionMarkCircleIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
</NavbarSection><NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem href="/help" aria-label="Help">
<QuestionMarkCircleIcon />
</NavbarItem>
<NavbarItem href="/notifications" aria-label="Notifications">
<BellIcon />
</NavbarItem>
</NavbarSection>