M
Marai.UI
v1.0.0-alpha.7

Navbar

A navigation system that provides toggle state, responsive show/hide, keyboard dismiss, and ARIA attributes — while giving you full visual control via Tailwind CSS or any CSS.

MNav vs MNavbar

Marai.UI has two separate navigation components for different use cases:

  • MNav (this page) — a stateful, composable system for responsive layouts. Use it when you need a hamburger-driven sidebar or collapsible menu. It owns open/close state and wires ARIA across multiple sub-components.
  • MNavbar — a lightweight, opinionated top navigation bar for simple site headers. Use it when you need a persistent horizontal top bar with brand, content slots, and action buttons, but no toggle behavior or sidebar.

Overview

The MNav system gives you full visual control while managing all navigation state and ARIA wiring. It owns state (is the sidebar open? is the menu open?) and exposes it to descendants via a cascading context. Developers control all visual aspects through CSS classes — the components set data-state="open" / data-state="closed" on elements so you can target state with standard CSS attribute selectors or Tailwind arbitrary variants like data-[state=open]:translate-x-0.

The system is composed of six components:

  • MNav — root; owns state and provides context to all children via CascadingValue
  • MNavBar — semantic <header> wrapper; zero behavior, pure structure
  • MNavToggle<button> wired to context; sets aria-expanded / aria-controls
  • MNavMenu<nav> with data-state; always in DOM; aria-hidden when closed
  • MNavSidebar<aside> with data-state; Escape key dismiss; optional focus-on-open
  • MNavOverlay — backdrop <div>; only rendered in DOM when any nav is open; click closes all

Usage

Add the namespace to your _Imports.razor:

razor
@using Marai.UI.Components.MNav

Sidebar Toggle (Mobile Drawer)

Click the hamburger to open the sidebar. Click the overlay or press Escape to close.

Preview
razor
<MNav class="relative h-50 overflow-hidden rounded-lg border border-slate-200">
    <MNavBar class="flex items-center h-14 px-4 gap-3 bg-primary text-white">
        <span class="font-bold text-[0.95rem]">My App</span>
        <MNavToggle Target="MNavTarget.Sidebar"
                    class="ml-auto bg-transparent border-0 text-white cursor-pointer p-1.5 rounded hover:bg-white/10 flex items-center justify-center">
            <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"
                 stroke-linecap="round" viewBox="0 0 24 24" aria-hidden="true">
                <line x1="3" y1="6" x2="21" y2="6"/>
                <line x1="3" y1="12" x2="21" y2="12"/>
                <line x1="3" y1="18" x2="21" y2="18"/>
            </svg>
        </MNavToggle>
    </MNavBar>
    <MNavSidebar class="absolute top-14 left-0 w-50 h-[calc(100%-3.5rem)] bg-white border-r border-slate-200 p-4
                        -translate-x-full data-[state=open]:translate-x-0 transition-transform duration-200">
        <nav class="flex flex-col gap-2 text-sm">
            <a href="#" class="text-slate-700 no-underline hover:text-slate-900">Home</a>
            <a href="#" class="text-slate-700 no-underline hover:text-slate-900">Docs</a>
            <a href="#" class="text-slate-700 no-underline hover:text-slate-900">Components</a>
        </nav>
    </MNavSidebar>
    <MNavOverlay class="absolute inset-0 bg-black/40" />
</MNav>

Target MNavTarget.Menu to toggle a horizontal menu panel instead of a sidebar.

Preview
razor
<MNav class="relative h-50 overflow-hidden rounded-lg border border-border">
    <MNavBar class="flex items-center h-14 px-4 gap-3 bg-primary text-white">
        <span class="font-bold text-[0.95rem]">My App</span>
        <MNavToggle Target="MNavTarget.Menu"
                    class="ml-auto bg-transparent border-0 text-white cursor-pointer p-1.5 rounded hover:bg-white/10 flex items-center justify-center">
            <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"
                 stroke-linecap="round" viewBox="0 0 24 24" aria-hidden="true">
                <line x1="3" y1="6" x2="21" y2="6"/>
                <line x1="3" y1="12" x2="21" y2="12"/>
                <line x1="3" y1="18" x2="21" y2="18"/>
            </svg>
        </MNavToggle>
    </MNavBar>
    <MNavMenu class="absolute top-14 left-0 right-0 bg-primary px-4 pb-4
                     scale-y-0 data-[state=open]:scale-y-100 origin-top transition-transform duration-150">
        <nav class="flex flex-col gap-1">
            <a href="#" class="text-white/80 no-underline text-sm py-1.5 border-t border-white/10 hover:text-white">Home</a>
            <a href="#" class="text-white/80 no-underline text-sm py-1.5 border-t border-white/10 hover:text-white">Docs</a>
            <a href="#" class="text-white/80 no-underline text-sm py-1.5 border-t border-white/10 hover:text-white">Components</a>
        </nav>
    </MNavMenu>
</MNav>

The data-state Pattern

MNavSidebar and MNavMenu always render in the DOM. They receive data-state="open" or data-state="closed" based on context state. Target these with CSS attribute selectors:

css
/* sidebar slides in from left on mobile */
.my-sidebar {
    transform: translateX(-100%);
    transition: transform 0.2s;
}
.my-sidebar[data-state=open] {
    transform: translateX(0);
}

Or with Tailwind CSS arbitrary variants in your class string:

razor
<MNavSidebar Class="fixed top-0 left-0 h-full w-64 z-50
                     -translate-x-full data-[state=open]:translate-x-0
                     transition-transform duration-200
                     lg:translate-x-0 lg:static lg:h-auto lg:z-auto">
    <!-- sidebar links -->
</MNavSidebar>

Full Layout Integration

Wrap your entire layout in MNav so the cascading context reaches both the navbar and the sidebar even when they live in separate child components:

razor
@* DocsLayout.razor — MNav wraps both TopNavbar and Sidebar *@
@inherits LayoutComponentBase
<MNav>
    <TopNavbar />
    <div class="docs-layout">
        <Sidebar />
        <main>@Body</main>
    </div>
</MNav>

@* TopNavbar.razor — reads MNavContext via CascadingParameter *@
<MNavBar class="...your-classes...">
    <a href="/">Logo</a>
    <MNavToggle Target="MNavTarget.Sidebar" class="... lg:hidden">
        <!-- hamburger icon -->
    </MNavToggle>
</MNavBar>

@* Sidebar.razor — MNavSidebar picks up state from parent MNav *@
<MNavSidebar class="fixed ... -translate-x-full data-[state=open]:translate-x-0 ...">
    <!-- nav links -->
</MNavSidebar>
<MNavOverlay class="fixed inset-0 bg-black/40 z-40 lg:hidden" />

Accessibility

  • MNavToggle sets aria-expanded="true"/"false" based on the target's open state
  • MNavToggle sets aria-controls="m-nav-menu" or aria-controls="m-nav-sidebar"
  • MNavMenu sets aria-hidden="true" when closed
  • MNavSidebar receives focus via Blazor's ElementReference.FocusAsync() when opened (set FocusOnOpen="false" to opt out)
  • Pressing Escape while focus is inside MNavSidebar closes all nav
  • MNavOverlay includes aria-hidden="true" and closes all nav on click

API Reference

MNav

ParameterTypeDefaultDescription
ChildContent RenderFragment? null All nav sub-components. State is cascaded to everything inside.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed to the root <div>.

MNavBar

ParameterTypeDefaultDescription
ChildContent RenderFragment? null Header content — brand, links, toggle button.
Class string? null CSS classes applied to the <header> element.
Style string? null Inline style applied to the <header> element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes passed through to <header>.

MNavToggle

ParameterTypeDefaultDescription
Target MNavTarget Sidebar Menu, Sidebar, or Both. Controls which nav panel is toggled.
ChildContent RenderFragment? null Button content — typically a hamburger SVG icon.
Class string? null CSS classes applied to the <button>.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes (e.g. aria-label).

MNavMenu

ParameterTypeDefaultDescription
ChildContent RenderFragment? null Menu links or content. Always in DOM; visibility controlled via data-state.
Class string? null CSS classes applied to the <nav id="m-nav-menu"> element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes.

MNavSidebar

ParameterTypeDefaultDescription
ChildContent RenderFragment? null Drawer content — nav links, sections, footer. Always in DOM.
FocusOnOpen bool true When true, the sidebar receives focus when it opens.
Class string? null CSS classes applied to the <aside id="m-nav-sidebar"> element.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes.

MNavOverlay

ParameterTypeDefaultDescription
Class string? null CSS classes applied to the overlay <div>. Only rendered when a nav panel is open.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes.

MNavTarget Enum

ValueDescription
MenuToggles the MNavMenu panel.
SidebarToggles the MNavSidebar drawer.
BothIf either is open, closes all. Otherwise opens the sidebar.