Navigation Menu
A horizontal nav menu for site headers. Ports the structural pieces of shadcn/ui's Radix Navigation Menu — no dropdown viewport.
Installation
bin/rails g shadcnrb:component navigation_menu
Example
<%= sui.navigation_menu do |nav| %> <%= nav.item { nav.link "Home", root_path } %> <%= nav.item { nav.link "Docs", docs_path } %> <%= nav.item { nav.link "Blocks", blocks_path } %> <%= nav.item { nav.link "GitHub", "https://github.com/accountaim/shadcn-rb", target: "_blank", rel: "noopener" } %> <% end %>
Item with dropdown (hover)
Pair nav.trigger with nav.content inside
a single nav.item. Opens on hover / focus, closes after a
short delay when the pointer leaves both the trigger and the content —
matching shadcn/ui's NavigationMenuTrigger. Click also toggles for touch.
<%= sui.navigation_menu do |nav| %> <%= nav.item { nav.link "Home", root_path } %> <%= nav.item do %> <%= nav.trigger value: "products" do %> Products <%= sui.icon :"chevron-down", class: "size-4 opacity-60 transition-transform group-data-[state=open]:rotate-180" %> <% end %> <%= nav.content value: "products" do %> <div class="grid gap-1 w-64"> <% [ ["Feature A", "Fast, reliable, and accessible."], ["Feature B", "Works on every device, out of the box."], ["Integrations", "Connect with the tools your team already uses."], ["All products", "Explore the full platform."] ].each do |(title, desc)| %> <%= link_to "#", class: "block rounded-md p-2 hover:bg-accent hover:text-accent-foreground" do %> <div class="text-sm font-medium leading-none"><%= title %></div> <p class="mt-1 text-xs text-muted-foreground line-clamp-2"><%= desc %></p> <% end %> <% end %> </div> <% end %> <% end %> <%= nav.item { nav.link "Pricing", "#" } %> <%= nav.item { nav.link "Docs", docs_path } %> <% end %>
With logo + centered items
Logo on the left, nav items centered, CTA on the right — classic marketing header.
<div class="flex w-full items-center gap-6"> <div class="flex items-center gap-2"> <div class="flex h-7 w-7 items-center justify-center rounded-md bg-primary text-primary-foreground text-sm font-bold">A</div> <span class="font-semibold">Acme</span> </div> <div class="flex-1 flex justify-center"> <%= sui.navigation_menu do |nav| %> <%= nav.item { nav.link "Features", "#" } %> <%= nav.item { nav.link "Pricing", "#" } %> <%= nav.item { nav.link "About", "#" } %> <% end %> </div> <%= sui.link_to "Get started", "#", variant: :default, size: :sm %> </div>
With user profile dropdown
App-shell top bar: brand + nav items on the left, account dropdown on the right.
<div class="flex w-full items-center gap-6"> <span class="font-semibold">Acme</span> <%= sui.navigation_menu do |nav| %> <%= nav.item { nav.link "Dashboard", "#", active: true } %> <%= nav.item { nav.link "Projects", "#" } %> <%= nav.item { nav.link "Team", "#" } %> <% end %> <div class="ml-auto"> <%= sui.dropdown_menu do |m| %> <%= m.trigger variant: :ghost, size: :"icon-sm", "aria-label": "Account" do %> <%= sui.avatar size: :sm do |a| %> <%= a.fallback "SC" %> <% end %> <% end %> <%= m.content class: "w-48" do %> <%= m.label "My Account" %> <%= m.separator %> <%= m.item "Profile", "#" %> <%= m.item "Billing", "#" %> <%= m.item "Team settings", "#" %> <%= m.separator %> <%= m.item "Log out", "#" %> <% end %> <% end %> </div> </div>
Branded / colored
Drop a bg-* utility on an outer wrapper and pass a
matching class: to each nav.link —
the link's default bg-background is overridden by
TailwindMerge when you provide your own.
<div class="flex w-full items-center gap-6 rounded-lg bg-primary px-4 py-2"> <div class="flex items-center gap-2 text-primary-foreground"> <div class="flex h-7 w-7 items-center justify-center rounded-md bg-primary-foreground text-primary text-sm font-bold">A</div> <span class="font-semibold">Acme</span> </div> <%= sui.navigation_menu do |nav| %> <%= nav.item do %> <%= nav.trigger value: "brand-product", class: branded_trigger do %> Product <%= sui.icon :"chevron-down", class: "size-4 opacity-70 transition-transform group-data-[state=open]:rotate-180" %> <% end %> <%= nav.content value: "brand-product" do %> <div class="grid gap-1 w-64"> <% [ ["Overview", "Everything Acme can do."], ["Features", "Individual capabilities in depth."], ["Integrations", "Connect with the tools your team uses."] ].each do |(title, desc)| %> <%= link_to "#", class: "block rounded-md p-2 hover:bg-accent hover:text-accent-foreground" do %> <div class="text-sm font-medium leading-none"><%= title %></div> <p class="mt-1 text-xs text-muted-foreground line-clamp-2"><%= desc %></p> <% end %> <% end %> </div> <% end %> <% end %> <%= nav.item { nav.link "Pricing", "#", class: branded_link } %> <%= nav.item { nav.link "Docs", "#", class: branded_link } %> <% end %> <%= sui.link_to "Sign in", "#", variant: :secondary, size: :sm, class: "ml-auto" %> </div>
Active state
Pass active: true explicitly or let it auto-detect via
current_page? when you pass a Rails path. The marketing
header on this site uses auto-detection — whichever page you're on gets
an accent background.
Composing with dropdown_menu
Need a dropdown under a nav item? Nest a dropdown_menu inside
navigation_menu_item. The wrapper is structural — it doesn't
assume a specific trigger type.
Composition — Rails helpers inside
The shortcut form renders an anchor directly. Pass a block to get a styled
wrapper and put link_to / button_to
inside with full Rails options.
sui.navigation_menu do |nav| nav.item do nav.link active: current_page?(docs_path) do link_to "Docs", docs_path, data: { turbo_frame: "_top" } end end end
API
| Method | Args | Description |
|---|---|---|
sui.navigation_menu | **opts | Root <nav> + <ul> wrapper; yields a nav proxy |
nav.item | **opts | Single <li> in the list |
nav.link | name, options, active:, **opts | Link inside an item. Auto-active via current_page? |