docs / sula / sula-nav

SulaNav

a row of views that behaves like one body of liquid: one view opens into its bar of section tabs, the rest sit collapsed as icon pills, and the shapes merge and split as you move between them.

intensity · assertsrsc · client

provenance

authored in usva

layer

sula

intensity

asserts · one per region. it is the focal point

composition

one per page, fixed in a header that centers itsearch and theme controls ride along as satellites, one material with the barnever a second sula in the nav's region; satellites exist so nothing sits beside itnot an in-page tab strip. SulaSegmented switches content, this switches routes

a11y

a labelled nav landmark · the active tab carries aria-current="page" · melted sides are inert · the canvas is aria-hiddenjest-axe

dependencies

motion · ogl · sula-core and sula-motion from the same package
live demo · try it out
customize
fluidfalse mounts no canvas, plain css pills
sidesOpenmelt brand and pills back into the active bar
satellitesa search and a theme control, one material with the bar
shine0 matte glass, 1 full neon rim
0.7
mergeRadiushow eagerly the parts merge, in px
14
revealDelayms after landing before the sides emerge
120
usagecopy
import { SulaNav } from "@usva-ui/react/sula/sula-nav";
import Link from "next/link";

<header className="fixed inset-x-0 top-0 z-50 flex justify-center p-4">
  <SulaNav
    linkComponent={Link}
    brand={<span>acme</span>}
    brandLabel="acme home"
    activeView={view}
    onViewChange={setView}
    activeItem={section}
    onNavigate={setSection}
    views={views}
  />
</header>

props

proptypedefaultnotes
viewsSulaNavView[]each entry is { href, label, icon, items? }. one expands to its bar of section tabs; the rest collapse to icon pills.
activeViewstringfirst viewcontrolled: the href of the expanded view. derive it from your router.
onViewChange(href: string) => voidfires when a collapsed view pill is clicked; the old bar melts down while the clicked one swells up.
activeItemstringcontrolled: the active section tab inside the expanded view. drive it from a scroll-spy.
onNavigate(href: string) => voidfires on a section-tab click, alongside the link's own navigation.
linkComponentReact.ElementType"a"pass next/link or a NavLink.
brand / brandHref / brandLabelReactNode · string · stringbrandHref: "/"the leftmost pill: a wordmark linking to brandHref. needs brandLabel to be named.
satellitesSulaNavSatellite[]fields that split off the body and settle in a corner: search, theme. handing them here keeps them one material with the bar.
labelsFrom"sm" | "md" | "lg" | "xl""sm"below this width the item labels fold away and the tabs are icons.
collapseBelow"sm" | "md" | "lg"below this width the routes and satellites fold into a single menu droplet and the body swells open into a panel. nothing is hidden.
menuLabelstring"Menu"accessible name of the menu droplet.
offsetnumber0vertical nudge in px from the nav's anchor: positive is down.
ariaLabelstring"Primary"names the nav landmark.
fluidbooleantruefalse renders plain CSS pills and mounts no canvas. reduced motion and missing WebGL2 take the same path.
backdrop / tint / accentColorstringbg · surface · accent tokenswhat the glass tints against, the glass itself, and the rim light. re-read on theme change.
shinenumbertheme0 is flat matte glass, 1 is the full neon rim. dark themes glow, pale ones stay subtle.
mergeRadiusnumber14how eagerly the parts merge, in pixels.
revealDelaynumber120milliseconds after the bar lands before the sides emerge.
sidesOpenbooleantruewhether the brand, pills and satellites are out. toggle from scroll to melt everything but the active bar back in; a hidden part is not tabbable.

get it

bun add @usva-ui/reactcopy
usagecopy
import { SulaNav } from "@usva-ui/react/sula/sula-nav";
import Link from "next/link";

<header className="fixed inset-x-0 top-0 z-50 flex justify-center p-4">
  <SulaNav
    linkComponent={Link}
    brand={<span>acme</span>}
    brandLabel="acme home"
    activeView={view}
    onViewChange={setView}
    activeItem={section}
    onNavigate={setSection}
    views={[
      {
        href: "/",
        label: "Site",
        icon: <HomeIcon />,
        items: [
          { href: "#home", label: "Home" },
          { href: "#work", label: "Work" },
        ],
      },
      { href: "/play", label: "Playground", icon: <SparkIcon /> },
    ]}
  />
</header>