Components · Navigation
Navigation menu
BetaTop-level site navigation with submenus.
- Source
components/ui/navigation-menu.tsx - shadcn/ui Navigation menu
- Figma: not linked yet (Q13)
When to use
- For the top navigation of public, marketing-style pages: the SuperCoach website, the help center, a coach's public profile and packages.
- When a few top-level sections each have a handful of pages that benefit from a short description: Getting started, Features.
coach-admin itself doesn't need it. The app's main navigation is the Sidebar, and in-page views use Tabs.
When not to use
- For coach-admin's main navigation: use the Sidebar.
- For a list of actions on an item (Edit, Duplicate, Archive): use a dropdown menu. Navigation menu items go to pages; they don't act.
- For more than about 5 top-level items, or deep trees: use a sidebar, or a page that lists the sections.
- On mobile: the bar doesn't fit. Use a Sheet with the same links.
Anatomy
- 1. Navigation menu: a nav landmark around a list; give it an aria-label
- 2. Trigger: a button that opens a submenu; the chevron turns when open
- 3. Link: a top-level page with no submenu, styled like a trigger
Open a trigger to see the rest: the content with links (a title and an optional one-line description each), and the viewport, the popover-colored panel that holds the open content. With the viewport (the default) one panel is shared and resizes as the coach moves between triggers; with viewport={false} each content opens in its own panel under its trigger. An optional indicator (NavigationMenuIndicator) points at the open trigger.
Variants
| Variant | Use for |
|---|---|
| With viewport (default) | Several submenus of different sizes. The shared panel animates between them, sliding in from the side the coach moved toward. |
Without viewport (viewport={false}) | One or two small submenus, or when the panel must sit right under its trigger. |
| Link only | A top-level page with no submenu ("Pricing"). Use NavigationMenuLink with navigationMenuTriggerStyle(). |
States
| State | What changes | Token |
|---|---|---|
| Rest | Page background | bg-background |
| Hover, focus | Accent fill | hover:bg-accent, focus:bg-accent, text-accent-foreground |
| Open | Lighter accent fill; chevron turns 180° | data-[state=open]:bg-accent/50, duration-(--duration-slow) |
Current page (active) | Lighter accent fill, aria-current="page" | data-[active=true]:bg-accent/50 |
| Focus-visible | Ring | ring-ring/50, 3px |
| Disabled | Half opacity | opacity-(--opacity-disabled) |
| Panel | Popover surface with border and shadow, on the dropdown layer | bg-popover, text-popover-foreground, border, shadow, z-(--z-dropdown) |
"Pricing" in the first preview is marked as the current page with active.
Usage guidelines
- 3 to 5 top-level items. Each submenu has 2 to 6 links; more than that wants its own page.
- Mark the section of the current page with
active, so it is visible and announced. - Every trigger opens a submenu, and every submenu link goes to a page. Don't put buttons, forms or search inside a submenu.
- Submenus open on hover and on click. Don't rely on hover alone for anything important; touch screens and keyboards don't hover.
Content guidelines
- Top-level labels are one or two words, nouns, sentence case: "Getting started", "Features", "Pricing".
- Link titles name the page; the description says what the coach finds there, in one short sentence: "Packages, invoices and payouts."
- Don't repeat the section in each link: under "Features", write "Programs", not "Programs feature".
- Arabic labels are short nouns too: "البدء"، "المزايا"، "الأسعار".
Accessibility
| Key | Does |
|---|---|
| Tab | Moves through the triggers and links, and into an open submenu |
| Enter / Space | Opens or closes the submenu of the focused trigger; opens a focused link |
| Arrow down | From an open trigger, moves into its submenu |
| Arrow left / right | Moves between top-level items. Reversed in RTL |
| Home / End | First / last top-level item |
| Escape | Closes the submenu and returns focus to its trigger |
- Rendered as a
navlandmark with a list. Name it witharia-label("Help center"), because a page often has more than onenav. - Triggers are buttons with
aria-expandedandaria-controls: "Features, collapsed, button". - Links marked
activegetaria-current="page": "Pricing, current page, link". - The submenu is not a
menurole: it's a group of ordinary links, so screen readers keep their normal link navigation. - Opening on hover waits 200ms, so moving the pointer across the bar doesn't flash panels open.
RTL
The bar reads from right to left, and arrow keys follow the reading order (the site provides Radix's DirectionProvider). The shared viewport sits at the start edge. Content slides in from the side the coach moved toward, which flips in RTL: the component has rtl: variants for each data-motion direction. The trigger chevron points down and does not mirror.
Tokens used
Read from components/ui/navigation-menu.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --accent | bg-accent bg-accent/50 | brand-50 | orange-50 |
| --accent-foreground | text-accent-foreground | brand-700 | orange-700 |
| --background | bg-background | white | white |
| --border | bg-border | neutral-200 | neutral-200 |
| --muted-foreground | text-muted-foreground | neutral-600 | neutral-600 |
| --popover | bg-popover | white | white |
| --popover-foreground | text-popover-foreground | neutral-950 | neutral-950 |
| --ring | ring-ring/50 | brand-600 | orange-500 |
- Layer
z-(--z-dropdown) - Layer
z-(--z-raised) - Motion
duration-(--duration-base) - Motion
duration-(--duration-slow) - Opacity
opacity-(--opacity-disabled) - Radius
rounded-md - Radius
rounded-sm rounded-ss-sm - Shadow
shadow-md - Type
font-medium - Type
text-sm
Props
<NavigationMenu>
Also takes every prop of Radix NavigationMenu.Root
| Prop | Type | Default |
|---|---|---|
| viewport? | boolean | true |
<NavigationMenuTrigger>
Also takes every prop of Radix NavigationMenu.Trigger
No props of its own.
<NavigationMenuContent>
Also takes every prop of Radix NavigationMenu.Content
No props of its own.
<NavigationMenuLink>
Also takes every prop of Radix NavigationMenu.Link
No props of its own.
<NavigationMenuIndicator>
Also takes every prop of Radix NavigationMenu.Indicator
No props of its own.
Replaces in coach-admin
From the read-only audit of coach-admin: 66 style values, 72 uses.
coach-admin's header is CoreUI's .navbar (brand, toggler, language switch and a banner). It is an app header, not a navigation menu: when it is rebuilt, its links belong in the Sidebar and its account actions in a dropdown menu. src/views/Base/Navbars is a CoreUI demo page (DECISIONS #10).
Classes and selectors
.navbar-brand12.navbar-toggler5.navbar .rtl3.nav-dropdown.open3.nav-dropdown-items2.navbar-divider1.navbar-toggler-icon1.navbar-nav .dropdown-item img, .exercises-page .view-action1.navbar-collapse li1.navbar i1.navbar-nav:has(.language-dropdown)1.navbar-collapse .form-inline1
Files
src/components/Header/header-banner.scss30src/components/Header/systemColorsConfig.js5src/components/Header/ChangeLang.css3src/views/Base/Navbars/Navbars.js1
The inventory lists the first 12 locations of each value, so file counts are a lower bound.
SCSS variables in these styles
$navbar-active-color→ nearest neutral-800 (no semantic token)$navbar-brand-widthnot a color: spacing, size or layout$navbar-brand-bgnot a color: spacing, size or layout$navbar-color→ nearest neutral-600 (no semantic token)$navbar-brand-logonot a color: spacing, size or layout$navbar-brand-minimized-bgnot a color: spacing, size or layout$navbar-brand-minimized-logonot a color: spacing, size or layout$navbar-toggler-iconnot a color: spacing, size or layout$sidebar-nav-dropdown-bgnot a color: spacing, size or layout$navbar-padding-ynot a color: spacing, size or layout$navbar-padding-xnot a color: spacing, size or layout$navbar-toggler-padding-ynot a color: spacing, size or layout