Skip to content
Gx
GitHub

Navigation Menu

A row of primary links.

<navigationmenu.NavigationMenu label="Main">
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuTrigger>Products</navigationmenu.NavigationMenuTrigger>
    <navigationmenu.NavigationMenuContent>
      <ul class="grid w-48 gap-1">
        <li>
          <navigationmenu.NavigationMenuLink href={gx.URL("/products")}>All products</navigationmenu.NavigationMenuLink>
        </li>
        <li>
          <navigationmenu.NavigationMenuLink href={gx.URL("/products/new")}>
            New arrivals
          </navigationmenu.NavigationMenuLink>
        </li>
      </ul>
    </navigationmenu.NavigationMenuContent>
  </navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuLink href={gx.URL("/")} variant={navigationmenu.Trigger} active>
      Home
    </navigationmenu.NavigationMenuLink>
  </navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuLink href={gx.URL("/docs")} variant={navigationmenu.Trigger}>
      Docs
    </navigationmenu.NavigationMenuLink>
  </navigationmenu.NavigationMenuItem>
</navigationmenu.NavigationMenu>

Installation

Run the command in the app module.

Terminal
gx add navigation-menu

The command also installs icons.

The command writes these files.

File Path in the app
NavigationMenu.gx ui/navigation-menu/NavigationMenu.gx
NavigationMenuContent.gx ui/navigation-menu/NavigationMenuContent.gx
NavigationMenuItem.gx ui/navigation-menu/NavigationMenuItem.gx
NavigationMenuLink.gx ui/navigation-menu/NavigationMenuLink.gx
NavigationMenuTrigger.gx ui/navigation-menu/NavigationMenuTrigger.gx
NavigationMenu.fixtures.go ui/navigation-menu/NavigationMenu.fixtures.go
NavigationMenuContent.fixtures.go ui/navigation-menu/NavigationMenuContent.fixtures.go
NavigationMenuItem.fixtures.go ui/navigation-menu/NavigationMenuItem.fixtures.go
NavigationMenuLink.fixtures.go ui/navigation-menu/NavigationMenuLink.fixtures.go
NavigationMenuTrigger.fixtures.go ui/navigation-menu/NavigationMenuTrigger.fixtures.go
styles.go ui/navigation-menu/styles.go

Install icons first.

Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.

ui/navigation-menu/NavigationMenu.gx
package navigationmenu

props {
  // Label is the accessible name of the nav element. It is not visible.
  Label    string = ""
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the items of the bar.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<nav aria-label={p.Label} class={gx.Cx("relative flex max-w-max flex-1 items-center justify-center", p.Class)} {...p.Attrs}>
  <ul class="flex flex-1 list-none items-center justify-center gap-1">{p.Children}</ul>
</nav>
ui/navigation-menu/NavigationMenuContent.gx
package navigationmenu

props {
  // Class adds classes to the panel element.
  Class    string = ""
  // Children is the content of the panel, usually links.
  Children gx.Node
  // Attrs adds HTML attributes to the panel element.
  Attrs    gx.Attrs = nil
}

<div class="invisible absolute top-full left-0 z-50 w-max origin-top-left scale-95 pt-1.5 opacity-0 transition-[opacity,scale,visibility] delay-150 duration-200 motion-reduce:transition-none group-hover/navigation-menu-item:visible group-hover/navigation-menu-item:scale-100 group-hover/navigation-menu-item:opacity-100 group-focus-within/navigation-menu-item:visible group-focus-within/navigation-menu-item:scale-100 group-focus-within/navigation-menu-item:opacity-100">
  <div class={gx.Cx("overflow-hidden rounded-md border border-border bg-popover p-2 pr-2.5 text-popover-foreground shadow", p.Class)} {...p.Attrs}>{p.Children}</div>
</div>
ui/navigation-menu/NavigationMenuItem.gx
package navigationmenu

props {
  // Class adds classes to the root element.
  Class    string = ""
  // Children is one link, or one trigger with its content.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<li class={gx.Cx("group/navigation-menu-item relative", p.Class)} {...p.Attrs}>{p.Children}</li>
ui/navigation-menu/NavigationMenuLink.gx
package navigationmenu

props {
  // Href is the URL that the link opens.
  Href     gx.URL
  // Variant sets the visual style: Default for a link in the content of a menu, or Trigger
  // for a link in the bar.
  Variant  Variant = Default
  // Active marks the link as the current page. It sets aria-current and the active style.
  Active   bool = false
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the link.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<a href={p.Href} aria-current={p.current()} class={gx.Cx("flex flex-col gap-1 rounded-sm p-2 text-sm no-underline transition-all outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", variantClass[p.variant()], activeClass[p.Active], p.Class)} {...p.Attrs}>{p.Children}</a>
ui/navigation-menu/NavigationMenuTrigger.gx
package navigationmenu

import "github.com/alternayte/gx/registry/icons"

props {
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the label of the trigger. A chevron follows it.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<button type="button" aria-haspopup="true" class={gx.Cx(triggerClass, "group-hover/navigation-menu-item:bg-accent/50 group-hover/navigation-menu-item:text-accent-foreground group-hover/navigation-menu-item:hover:bg-accent group-hover/navigation-menu-item:focus:bg-accent group-focus-within/navigation-menu-item:bg-accent/50 group-focus-within/navigation-menu-item:text-accent-foreground group-focus-within/navigation-menu-item:hover:bg-accent group-focus-within/navigation-menu-item:focus:bg-accent", p.Class)} {...p.Attrs}>
  {p.Children}
  <icons.ChevronDown class="relative top-[1px] ml-1 size-3 transition duration-300 group-hover/navigation-menu-item:rotate-180 group-focus-within/navigation-menu-item:rotate-180 motion-reduce:transition-none" />
</button>
ui/navigation-menu/NavigationMenu.fixtures.go
package navigationmenu

import "github.com/alternayte/gx"

var NavigationMenuFixtures = gx.Fixtures[NavigationMenuProps]{
	"Default": {Label: "Main", Children: gx.Frag(
		NavigationMenuItem(NavigationMenuItemProps{Children: gx.Frag(
			NavigationMenuTrigger(NavigationMenuTriggerProps{Children: gx.Text("Products")}),
			NavigationMenuContent(NavigationMenuContentProps{Children: gx.El("ul", gx.Attrs{{Key: "class", Value: "grid w-48 gap-1"}},
				gx.El("li", nil, NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/products"), Children: gx.Text("All products")})),
				gx.El("li", nil, NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/products/new"), Children: gx.Text("New arrivals")})),
			)}),
		)}),
		NavigationMenuItem(NavigationMenuItemProps{Children: NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/"), Variant: Trigger, Active: true, Children: gx.Text("Home")})}),
		NavigationMenuItem(NavigationMenuItemProps{Children: NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/docs"), Variant: Trigger, Children: gx.Text("Docs")})}),
	)},
}
ui/navigation-menu/NavigationMenuContent.fixtures.go
package navigationmenu

import "github.com/alternayte/gx"

var NavigationMenuContentFixtures = gx.Fixtures[NavigationMenuContentProps]{
	"Default": {Children: gx.El("ul", gx.Attrs{{Key: "class", Value: "grid w-48 gap-1"}},
		gx.El("li", nil, NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/products"), Children: gx.Text("All products")})),
		gx.El("li", nil, NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/products/new"), Children: gx.Text("New arrivals")})),
	)},
}

// NavigationMenuContentWrap renders the content under a trigger, as a page
// uses it. Hover or focus the trigger to show the content.
func NavigationMenuContentWrap(n gx.Node) gx.Node {
	return NavigationMenu(NavigationMenuProps{Label: "Content", Children: NavigationMenuItem(NavigationMenuItemProps{Children: gx.Frag(
		NavigationMenuTrigger(NavigationMenuTriggerProps{Children: gx.Text("Products")}),
		n,
	)})})
}
ui/navigation-menu/NavigationMenuItem.fixtures.go
package navigationmenu

import "github.com/alternayte/gx"

var NavigationMenuItemFixtures = gx.Fixtures[NavigationMenuItemProps]{
	"Link": {Children: NavigationMenuLink(NavigationMenuLinkProps{Href: gx.URL("/docs"), Variant: Trigger, Children: gx.Text("Docs")})},
}

// NavigationMenuItemWrap renders the item inside a menu, as a page uses it.
func NavigationMenuItemWrap(n gx.Node) gx.Node {
	return NavigationMenu(NavigationMenuProps{Label: "Item", Children: n})
}
ui/navigation-menu/NavigationMenuLink.fixtures.go
package navigationmenu

import "github.com/alternayte/gx"

var NavigationMenuLinkFixtures = gx.Fixtures[NavigationMenuLinkProps]{
	"Link":          {Href: gx.URL("/docs"), Children: gx.Text("Docs")},
	"Active":        {Href: gx.URL("/"), Active: true, Children: gx.Text("Home")},
	"Trigger":       {Href: gx.URL("/docs"), Variant: Trigger, Children: gx.Text("Docs")},
	"TriggerActive": {Href: gx.URL("/"), Variant: Trigger, Active: true, Children: gx.Text("Home")},
}
ui/navigation-menu/NavigationMenuTrigger.fixtures.go
package navigationmenu

import "github.com/alternayte/gx"

var NavigationMenuTriggerFixtures = gx.Fixtures[NavigationMenuTriggerProps]{"Default": {Children: gx.Text("Products")}}
ui/navigation-menu/styles.go
package navigationmenu

import "github.com/alternayte/gx"

// Variant is the visual style of a navigation link.
type Variant string

// The variants of navigationmenu.NavigationMenuLink. Default is a link
// inside the content of a menu. Trigger is a link in the bar; it takes the
// style of a trigger.
const (
	Default Variant = "default"
	Trigger Variant = "trigger"
)

// triggerClass is the style of a trigger and of a link in the bar.
const triggerClass = "inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium no-underline transition-[color,box-shadow] outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50"

var variantClass = gx.Enum[Variant]{
	Default: "",
	Trigger: triggerClass,
}

var activeClass = map[bool]string{
	true:  "bg-accent/50 text-accent-foreground hover:bg-accent focus:bg-accent",
	false: "",
}

// variant returns the variant of one link; a zero value is Default.
func (p NavigationMenuLinkProps) variant() Variant {
	if p.Variant == "" {
		return Default
	}
	return p.Variant
}

// current returns the aria-current value of one link.
func (p NavigationMenuLinkProps) current() string {
	if p.Active {
		return "page"
	}
	return ""
}

The theme must define these tokens: --border, --background, --popover, --popover-foreground, --accent, --accent-foreground, --ring.

Usage

A row of primary links and menus of links.

<navigationmenu.NavigationMenu label="Main">
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuTrigger>Products</navigationmenu.NavigationMenuTrigger>
    <navigationmenu.NavigationMenuContent>
      <ul class="grid w-48 gap-1">
        <li><navigationmenu.NavigationMenuLink href={gx.URL("/products")}>All products</navigationmenu.NavigationMenuLink></li>
        <li><navigationmenu.NavigationMenuLink href={gx.URL("/products/new")}>New arrivals</navigationmenu.NavigationMenuLink></li>
      </ul>
    </navigationmenu.NavigationMenuContent>
  </navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuLink href={gx.URL("/docs")} variant={navigationmenu.Trigger} active={true}>Docs</navigationmenu.NavigationMenuLink>
  </navigationmenu.NavigationMenuItem>
</navigationmenu.NavigationMenu>

NavigationMenu renders the nav and its list. Each NavigationMenuItem holds one link, or one trigger with its content. A link in the bar takes variant={navigationmenu.Trigger}, the style of a trigger.

The content is CSS only. It shows when the pointer is on the item and when focus is in the item, below its own item. It has no shared viewport and no indicator.

Examples

<navigationmenu.NavigationMenu label="Main">
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuTrigger>Products</navigationmenu.NavigationMenuTrigger>
    <navigationmenu.NavigationMenuContent>
      <ul class="grid w-48 gap-1">
        <li>
          <navigationmenu.NavigationMenuLink href={gx.URL("/products")}>All products</navigationmenu.NavigationMenuLink>
        </li>
        <li>
          <navigationmenu.NavigationMenuLink href={gx.URL("/products/new")}>
            New arrivals
          </navigationmenu.NavigationMenuLink>
        </li>
      </ul>
    </navigationmenu.NavigationMenuContent>
  </navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuLink href={gx.URL("/")} variant={navigationmenu.Trigger} active>
      Home
    </navigationmenu.NavigationMenuLink>
  </navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuItem>
    <navigationmenu.NavigationMenuLink href={gx.URL("/docs")} variant={navigationmenu.Trigger}>
      Docs
    </navigationmenu.NavigationMenuLink>
  </navigationmenu.NavigationMenuItem>
</navigationmenu.NavigationMenu>
<navigationmenu.NavigationMenuContent>
  <ul class="grid w-48 gap-1">
    <li>
      <navigationmenu.NavigationMenuLink href={gx.URL("/products")}>All products</navigationmenu.NavigationMenuLink>
    </li>
    <li>
      <navigationmenu.NavigationMenuLink href={gx.URL("/products/new")}>New arrivals</navigationmenu.NavigationMenuLink>
    </li>
  </ul>
</navigationmenu.NavigationMenuContent>
<navigationmenu.NavigationMenuItem>
  <navigationmenu.NavigationMenuLink href={gx.URL("/docs")} variant={navigationmenu.Trigger}>
    Docs
  </navigationmenu.NavigationMenuLink>
</navigationmenu.NavigationMenuItem>
<navigationmenu.NavigationMenuLink href={gx.URL("/docs")}>Docs</navigationmenu.NavigationMenuLink>
<navigationmenu.NavigationMenuLink href={gx.URL("/")} active>Home</navigationmenu.NavigationMenuLink>
<navigationmenu.NavigationMenuLink href={gx.URL("/docs")} variant={navigationmenu.Trigger}>
  Docs
</navigationmenu.NavigationMenuLink>
<navigationmenu.NavigationMenuLink href={gx.URL("/")} variant={navigationmenu.Trigger} active>
  Home
</navigationmenu.NavigationMenuLink>
<navigationmenu.NavigationMenuTrigger>Products</navigationmenu.NavigationMenuTrigger>

API reference

A tag sets a prop by its name with a lower-case first letter: Class is class.

Prop Type Default Description
Label string "" Label is the accessible name of the nav element. It is not visible.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the items of the bar.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.
Prop Type Default Description
Class string "" Class adds classes to the panel element.
Children gx.Node Required Children is the content of the panel, usually links.
Attrs gx.Attrs nil Attrs adds HTML attributes to the panel element.
Prop Type Default Description
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is one link, or one trigger with its content.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.
Prop Type Default Description
Href gx.URL Required Href is the URL that the link opens.
Variant Variant Default Variant sets the visual style: Default for a link in the content of a menu, or Trigger for a link in the bar.
Active bool false Active marks the link as the current page. It sets aria-current and the active style.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the content of the link.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.
Prop Type Default Description
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the label of the trigger. A chevron follows it.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Pass a typed route to Href.
  • Mark the current page with Active.
  • Give the menu a Label when a page has more than one nav.

Don't

  • Do not use it for a footer row of links.
  • Do not put an action in the content. Use a dropdown menu.

Keyboard

Key Action
Tab Moves between the links and triggers. The content of a focused trigger shows.
Tab From a trigger, moves into its content. The content closes when focus leaves the item.
Enter Follows the focused link.