Skip to content

PlNavigationMenu

A site's navigation: a row of destinations, some of which open a panel of more of them. Every row is a real link, which is the whole reason this is not a menu.

React
tsx
import { PlNavigationMenu, PlNavigationMenuItem, PlNavigationMenuLink } from 'plass-ui';

<PlNavigationMenu>
  <PlNavigationMenuItem label="Product" columns={2}>
    <PlNavigationMenuLink href="/analytics" title="Analytics" description="Numbers over time" />
  </PlNavigationMenuItem>
  <PlNavigationMenuItem label="Pricing" href="/pricing" />
</PlNavigationMenu>;
dart
import 'package:plass_ui/plass_ui.dart';

PlNavigationMenu(
  items: <PlNavigationMenuItem>[
    PlNavigationMenuItem(
      label: 'Product',
      columns: 2,
      links: <PlNavigationMenuLink>[
        PlNavigationMenuLink(title: 'Analytics', onPressed: openAnalytics),
      ],
    ),
    PlNavigationMenuItem(label: 'Pricing', onPressed: openPricing),
  ],
);

Props

PropTypeDefaultDescription
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The row's height and type scale, and the panel's radius and padding with it
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. It reaches the hover, the open item and the focus rings; the sheet is never dyed
densityshared'default' | 'compact''default'Padding only
orientationshared'horizontal' | 'vertical''horizontal'Which way the row runs. vertical is a nav rail whose panels open beside it
valuestring | nullWhich item's panel is open, by its value. Nullish means closed
defaultValuestring | nullWhich starts open, for an uncontrolled menu
onValueChange(value: string | null) => voidCalled when the open panel changes
delaynumberHow long the pointer rests before a panel opens, in milliseconds
closeDelaynumberHow long a panel stays after the pointer leaves, in milliseconds
sideOffsetnumber8Distance from the row, in pixels
childrenReactNodeThe items
PropTypeDefaultDescription
sizesharedPlassSizePlassSize.mdThe row's height and type scale, and the panel's radius and padding with it
colorsharedPlassColorPlassColor.primarySemantic colour role. It reaches the hover, the open item and the focus rings; the sheet is never dyed
densitysharedPlassDensityPlassDensity.standardPadding only
items * List<PlNavigationMenuItem>The items — data here rather than composed children, because the row has to know which item is which to open one panel at a time
initialValueString?Which item's panel starts open. There is no controlled mode: which panel is open is the pointer's and the keyboard's, not the app's data
onValueChangedValueChanged<String?>?Called when the open panel changes
orientationsharedPlassOrientationPlassOrientation.horizontalWhich way the row runs. vertical is a nav rail whose panels open beside it
delayDurationDuration(milliseconds: 50)How long the pointer rests before a panel opens, in milliseconds
closeDelayDurationDuration(milliseconds: 100)How long a panel stays after the pointer leaves, in milliseconds
sideOffsetdouble8Distance from the row, in pixels
semanticLabelString?The name the navigation region is announced by. Required when a screen has more than one

Every native <nav> attribute passes straight through. color is excluded because it is a Plass prop here, and defaultValue / onChange because the menu spells them as a value and an onValueChange.

PlNavigationMenuItem

PropTypeDefaultDescription
label * ReactNodeThe word in the row
hrefstringMakes the item a plain link rather than something that opens a panel. An item with an href and no children is a destination, and it is announced as one
targetstringWhere the link opens. Anything other than this tab also gets noopener noreferrer merged into its rel
relstringThe link's rel
startIconReactNodeContent before the label
valuestringIdentifies the item, for a controlled menu
disabledbooleanfalseUnavailable. The word stays in the row and opens nothing
columnsnumber1How many columns the panel lays its links out in
childrenReactNodeThe panel's contents — usually PlNavigationMenuLinks
classNamestringClasses on the word in the row, alongside the component's own rather than in place of them
styleCSSPropertiesInline styles on the word in the row, applied over the custom properties it wrote
PropTypeDefaultDescription
label * StringThe word in the row
valueString?Identifies the item in the menu's value. Left out, the label is used
onPressedVoidCallback?Makes the item a destination rather than something that opens a panel. There is no href here: where a destination is belongs to the app's own router
startIconWidget?Content before the label
disabledboolfalseUnavailable. The word stays in the row and opens nothing
columnsint1How many columns the panel lays its links out in
linksList<PlNavigationMenuLink>const []The panel's contents — usually PlNavigationMenuLinks
PropTypeDefaultDescription
href * stringWhere it goes
title * ReactNodeThe row's name
descriptionReactNodeA second line under it, one step down the scale and muted
startIconReactNodeA glyph before the title
PropTypeDefaultDescription
title * StringThe row's name
descriptionString?A second line under it, one step down the scale and muted
startIconWidget?A glyph before the title
onPressed * VoidCallback?Where it goes. There is no navigator in this package, so this is where that is decided

What the shared axes mean across the library is in prop conventions.

PlNavigationMenu or PlMenu

The difference is what the rows are.

A PlMenu holds actions. Its rows are menuitems, and the whole thing is a widget that traps the arrow keys and closes when one is chosen.

This holds links. It is a <nav> full of real <a>s, which is what puts them in the browser's link list, on the status bar under the pointer, in the middle-click menu and in a crawler's index. A destination that is a <div> with a click handler is in none of those.

Reach for a menu when the row does something. Reach for this when the row goes somewhere.

Examples

An item with an href and no children is a link. One with children is a trigger and a panel.

The difference is not cosmetic: the first is announced as a destination and the second as something that expands, so a screen reader tells a reader which of the two they are about to press.

React

columns

How many columns the panel lays its links out in. A PlNavigationMenuLink is one row: a title, an optional muted description under it and an optional glyph before it.

One panel is open at a time and it resizes between items rather than closing and reopening, which is what makes crossing the row read as one surface rather than three.

React

orientation

vertical is a nav rail whose panels open beside it rather than under it. The arrow keys follow either way.

React

The row's surface

At rest the items are the page's own words: no fill, no edge, no shadow. Five bordered boxes across the top of a site is a toolbar rather than a navigation, and a navigation should read as text until it is reached for.

The family arrives with the pointer and with the open panel, and the sheet itself is never dyed. The panel is the same frosted glass a PlMenu and a PlPopover draw.

target on an item does what it does on an <a>, and anything other than this tab has noopener noreferrer merged into whatever rel was asked for.

Merged rather than replaced: the common reason to write a rel by hand is nofollow or sponsored, and spelling that as an override would silently take the protection off a link that still opens elsewhere.

Differences from the React build

ReactFlutterWhy
href on an item and on a linkonPressedThere is no navigator in this package and no address to resolve. Where a destination is belongs to the app's own router.
composed PlNavigationMenuItem childrenitems: List<PlNavigationMenuItem> as dataThe row has to know which item is which to keep one panel open at a time, and a list is what it can count.
value / defaultValueinitialValueString? has no way to tell "the caller did not say" from "the caller says closed", so a controlled mode would be one that could never be closed from outside. Which panel is open is the pointer's state, not the app's.
one panel that resizes between itemsone panel per item, fadingThe resize is Base UI measuring the outgoing and incoming panels and animating between them. Here each item anchors its own popup, so crossing the row swaps panels rather than growing one.
target and a merged relThere is no rel to protect, because there is no anchor.
the <nav> landmarkSemanticsRole.navigationThe same landmark under the framework's own name.
className, style, native attributesThere is no class list and no style attribute to pass through.

Accessibility

  • It is a real <nav> full of real <a>s. That is the component's whole argument, and everything below follows from it.
  • Base UI owns the keyboard: the arrow keys move along the row, Enter and Space open a panel, Esc closes it and focus returns to the trigger, and Tab moves into an open panel's links.
  • A trigger reports aria-expanded, so a reader is told what pressing it will do.
  • A disabled item keeps its word in the row and opens nothing. It is dimmed rather than recoloured, which is what disabled looks like everywhere in the library.
  • The popup is portalled to the end of <body> and its positioner carries .plass-portal, which is where a host that scopes a CSS reset hangs the same reset.
  • The chevron turns rather than the panel sliding. Nothing here moves under the pointer.

Released under the MIT License