Kosmesis
Components

Navigation Menu

Site navigation with submenus, wrapping Morphos's NavigationMenu primitive.

Installation

npx kosmesis add navigation-menu
pnpm dlx kosmesis add navigation-menu
yarn dlx kosmesis add navigation-menu
bunx kosmesis add navigation-menu

Install the following dependencies:

npm install @morphos/layout
pnpm add @morphos/layout
yarn add @morphos/layout
bun add @morphos/layout

Copy and paste the following code into your project.

navigation-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import {  NavigationMenu as MorphosNavigationMenu,  NavigationMenuContent as MorphosNavigationMenuContent,  NavigationMenuItem as MorphosNavigationMenuItem,  NavigationMenuLink as MorphosNavigationMenuLink,  NavigationMenuList as MorphosNavigationMenuList,  NavigationMenuTrigger as MorphosNavigationMenuTrigger,  type NavigationMenuContentProps as MorphosNavigationMenuContentProps,  type NavigationMenuLinkProps as MorphosNavigationMenuLinkProps,  type NavigationMenuListProps as MorphosNavigationMenuListProps,  type NavigationMenuTriggerProps as MorphosNavigationMenuTriggerProps} from "@morphos/layout";import { cn } from "@/lib/utils";/** * Extends (not wraps) Morphos's `NavigationMenu` so `new NavigationMenu()` still yields a real * instance with `.activeItem`/`.open()`/`.close()` — what `NavigationMenuItem` needs via its * `nav` prop. */@Component()export class NavigationMenu extends MorphosNavigationMenu {  render() {    return (      <nav        id={this.id}        class={cn("relative flex max-w-max flex-1 items-center justify-center", this.class)}        aria-label={this["aria-label"]}        data-orientation={this.orientation}      >        {this.children}      </nav>    );  }}export type NavigationMenuListProps = MorphosNavigationMenuListProps;@Component()export class NavigationMenuList extends StatelessComponent<NavigationMenuListProps> {  render() {    const { class: cls, ...rest } = this.props;    return <MorphosNavigationMenuList class={cn("group flex flex-1 list-none items-center justify-center gap-1", cls)} {...rest} />;  }}/** * Extends (not wraps) Morphos's `NavigationMenuItem` — its `Trigger`/`Content` reference this * exact instance via their `item` prop, the same "instantiate once, pass to siblings" pattern as * the top-level `nav` root (see the real usage note on `NavigationMenuTrigger` below). */@Component()export class NavigationMenuItem extends MorphosNavigationMenuItem {  render() {    return (      <li id={this.id} class={cn("relative", this.class)} data-active={this.isOpen ? "" : undefined}>        {this.children}      </li>    );  }}export const navigationMenuTriggerStyle = () =>  "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-active:bg-accent/50 disabled:pointer-events-none disabled:opacity-50";/** * `item` must be the same `NavigationMenuItem` instance mounted as the enclosing `<li>` — create * it once (`@State() productsItem = new NavigationMenuItem({ nav: this.nav, value: "products" })`) * and pass it to `NavigationMenuItem`, `NavigationMenuTrigger`, and `NavigationMenuContent` alike. */export type NavigationMenuTriggerProps = MorphosNavigationMenuTriggerProps;@Component()export class NavigationMenuTrigger extends StatelessComponent<NavigationMenuTriggerProps> {  render() {    const { class: cls, children, ...rest } = this.props;    return (      <MorphosNavigationMenuTrigger class={cn(navigationMenuTriggerStyle(), "group", cls)} {...rest}>        {children}      </MorphosNavigationMenuTrigger>    );  }}export type NavigationMenuContentProps = MorphosNavigationMenuContentProps;@Component()export class NavigationMenuContent extends StatelessComponent<NavigationMenuContentProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosNavigationMenuContent        class={cn(          "absolute top-full left-0 z-40 mt-1.5 w-auto min-w-48 rounded-md border bg-popover p-2 text-popover-foreground shadow-md",          cls,        )}        {...rest}      />    );  }}export type NavigationMenuLinkProps = MorphosNavigationMenuLinkProps;@Component()export class NavigationMenuLink extends StatelessComponent<NavigationMenuLinkProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosNavigationMenuLink        class={cn(          "flex flex-col gap-1 rounded-sm p-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",          cls,        )}        {...rest}      />    );  }}

Install the following dependencies:

npm install @morphos/layout @praxisjs/css
pnpm add @morphos/layout @praxisjs/css
yarn add @morphos/layout @praxisjs/css
bun add @morphos/layout @praxisjs/css

Copy and paste the following code into your project.

navigation-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import {  NavigationMenu as MorphosNavigationMenu,  NavigationMenuContent as MorphosNavigationMenuContent,  NavigationMenuItem as MorphosNavigationMenuItem,  NavigationMenuLink as MorphosNavigationMenuLink,  NavigationMenuList as MorphosNavigationMenuList,  NavigationMenuTrigger as MorphosNavigationMenuTrigger,  type NavigationMenuContentProps as MorphosNavigationMenuContentProps,  type NavigationMenuLinkProps as MorphosNavigationMenuLinkProps,  type NavigationMenuListProps as MorphosNavigationMenuListProps,  type NavigationMenuTriggerProps as MorphosNavigationMenuTriggerProps} from "@morphos/layout";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class NavigationMenuStyles extends Stylesheet {  $root = this.css({ position: "relative", display: "flex", maxWidth: "max-content", flex: "1 1 0%", alignItems: "center", justifyContent: "center" });  $list = this.css({ display: "flex", flex: "1 1 0%", listStyle: "none", alignItems: "center", justifyContent: "center", gap: "0.25rem" });  $item = this.css({ position: "relative" }).on("&[data-active]", { fontWeight: 500 });  $trigger = this.css({    display: "inline-flex",    height: "2.25rem",    width: "max-content",    alignItems: "center",    justifyContent: "center",    gap: "0.375rem",    borderRadius: `calc(${t.radius} - 2px)`,    backgroundColor: t.background,    padding: "0.5rem 1rem",    fontSize: "0.875rem",    fontWeight: 500,    transition: "background-color 120ms ease, color 120ms ease",  })    .hover({ backgroundColor: t.accent, color: t.accentForeground })    .focus({ backgroundColor: t.accent, color: t.accentForeground })    .disabled({ pointerEvents: "none", opacity: 0.5 });  $content = this.css({    position: "absolute",    top: "100%",    left: "0",    zIndex: 40,    marginTop: "0.375rem",    width: "auto",    minWidth: "12rem",    borderRadius: `calc(${t.radius} - 2px)`,    border: `1px solid ${t.border}`,    backgroundColor: t.popover,    padding: "0.5rem",    color: t.popoverForeground,    boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",  });  $link = this.css({    display: "flex",    flexDirection: "column",    gap: "0.25rem",    borderRadius: "0.125rem",    padding: "0.5rem",    fontSize: "0.875rem",    outline: "none",    transition: "background-color 120ms ease, color 120ms ease",  })    .hover({ backgroundColor: t.accent, color: t.accentForeground })    .focus({ backgroundColor: t.accent, color: t.accentForeground });}/** * Extends (not wraps) Morphos's `NavigationMenu` so `new NavigationMenu()` still yields a real * instance with `.activeItem`/`.open()`/`.close()` — what `NavigationMenuItem` needs via its * `nav` prop. */@Component()export class NavigationMenu extends MorphosNavigationMenu {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    return (      <nav id={this.id} class={cx(this.$s.$root, this.class)} aria-label={this["aria-label"]} data-orientation={this.orientation}>        {this.children}      </nav>    );  }}export type NavigationMenuListProps = MorphosNavigationMenuListProps;@Component()export class NavigationMenuList extends StatelessComponent<NavigationMenuListProps> {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosNavigationMenuList class={cx(this.$s.$list, cls)} {...rest} />;  }}/** * Extends (not wraps) Morphos's `NavigationMenuItem` — its `Trigger`/`Content` reference this * exact instance via their `item` prop, the same "instantiate once, pass to siblings" pattern as * the top-level `nav` root. */@Component()export class NavigationMenuItem extends MorphosNavigationMenuItem {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    return (      <li id={this.id} class={cx(this.$s.$item, this.class)} data-active={this.isOpen ? "" : undefined}>        {this.children}      </li>    );  }}/** * `item` must be the same `NavigationMenuItem` instance mounted as the enclosing `<li>` — create * it once (`@State() productsItem = new NavigationMenuItem({ nav: this.nav, value: "products" })`) * and pass it to `NavigationMenuItem`, `NavigationMenuTrigger`, and `NavigationMenuContent` alike. */export type NavigationMenuTriggerProps = MorphosNavigationMenuTriggerProps;@Component()export class NavigationMenuTrigger extends StatelessComponent<NavigationMenuTriggerProps> {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    const { class: cls, children, ...rest } = this.props;    return (      <MorphosNavigationMenuTrigger class={cx(this.$s.$trigger, cls)} {...rest}>        {children}      </MorphosNavigationMenuTrigger>    );  }}export type NavigationMenuContentProps = MorphosNavigationMenuContentProps;@Component()export class NavigationMenuContent extends StatelessComponent<NavigationMenuContentProps> {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosNavigationMenuContent class={cx(this.$s.$content, cls)} {...rest} />;  }}export type NavigationMenuLinkProps = MorphosNavigationMenuLinkProps;@Component()export class NavigationMenuLink extends StatelessComponent<NavigationMenuLinkProps> {  @Styled(NavigationMenuStyles) $s!: NavigationMenuStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosNavigationMenuLink class={cx(this.$s.$link, cls)} {...rest} />;  }}

Examples

Usage

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";

@Component()
class SiteNav extends StatefulComponent {
  @State() nav = new NavigationMenu();
  @State() productsItem = new NavigationMenuItem({ nav: this.nav, value: "products" });

  render() {
    return (
      <NavigationMenu aria-label="Main navigation">
        <NavigationMenuList nav={this.nav}>
          <NavigationMenuItem nav={this.nav} value="products">
            <NavigationMenuTrigger item={this.productsItem}>Products</NavigationMenuTrigger>
            <NavigationMenuContent item={this.productsItem}>
              <NavigationMenuLink href="/analytics">Analytics</NavigationMenuLink>
              <NavigationMenuLink href="/automation">Automation</NavigationMenuLink>
            </NavigationMenuContent>
          </NavigationMenuItem>
        </NavigationMenuList>
      </NavigationMenu>
    );
  }
}

Each NavigationMenuItem needs its own sibling instance (this.productsItem) — one per submenu — the same "instantiate once, pass to every part" pattern as the top-level nav.

Props

PropTypeDefault
orientation"horizontal" | "vertical"
aria-labelstring
PropTypeDefault
navNavigationMenu— required
PropTypeDefault
navNavigationMenu— required
valuestring— required
PropTypeDefault
itemNavigationMenuItem— required
PropTypeDefault
itemNavigationMenuItem— required
PropTypeDefault
hrefstring
targetstring
relstring
onClick(event: MouseEvent) => void

On this page