GrNavbar

Package: @feugene/granularitycoreGroup: navigation

Top navigation for the main sections and global actions.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • the application has a top bar — a title, search, a profile, notifications in three zones;
  • the bar has to stay visiblesticky while scrolling long pages;
  • a menu button is needed on mobileshowMenuButton opens a GrDrawer or a GrSidebar;
  • a banner landmark is needed — the root renders as a <header> tag, and walking by landmarks works.

When to take something else

NeedTake
Side navigationGrSidebar
A bottom bar of sections on mobileGrBottomNav
A bar of actions above a table or a dashboardGrButtonGroup / GrDashboardToolbar
The path to the current pageGrBreadcrumbs

The three zones

<GrNavbar>
  <template #title>
    <RouterLink to="/">Granularity</RouterLink>
  </template>

  <template #center>
    <GrInput placeholder="Search" size="sm" aria-label="Search" />
  </template>

  <GrButton size="sm">Publish</GrButton>
</GrNavbar>
SlotWhere
titleon the left, after the menu button; replaces the title string
leftright after the title — tabs, a section switch
centerin the middle of the bar — search, breadcrumbs
the default oneon the right — actions, the avatar

The title prop is optional: the title is assembled as markup through the slot. When there is neither a string nor a slot, the title block is not rendered and does not eat up the padding.

The central zone appears only together with the slot. With it the side zones divide the remainder equally — otherwise the “centre” would be computed from the remainder and would drift after the wider side.

Sticking

<GrNavbar sticky />

sticky keeps the bar at the top while scrolling. The layer is --gr-z-navbar (900): below the anchored panels (--gr-z-dropdown — 1000), so an open list, a tooltip or a modal covers the bar rather than moving under it. The details — ../z-index.md.

The height

It is set by the --gr-navbar-height variable (56px by default) — on the bar itself, on an ancestor or in the theme:

.app-shell { --gr-navbar-height: 48px; }

The menu button

showMenuButton shows the button and emits menu; its accessible name comes from the locale (gr.navbar.openMenu). menuButtonClass is needed to hide the button on wide screens (sm:hidden) — the bar does not decide on the application’s behalf when the menu folds away.

size sets the step of that button only — everything else in the header belongs to the consumer, and the height of the row is held by --gr-navbar-height. It is read from GrConfigProvider (componentDefaults.GrNavbar.size).

The fallback is sm (32px) rather than the md shared by the package: the button has been that size from the very beginning, and raising the default would shift the header for everyone. If a proper touch target is needed, size="lg" gives 44px and fits into a row 56px tall.

Playground 5

Loading…

Code
<GrNavbar />

Install

npm i @feugene/granularity

Import

import { GrNavbar } from '@feugene/granularity/components/GrNavbar'

API

Props

PropTypedefaultDescription
titlestring | undefinedundefinedThe heading as a string. The `#title` slot is stronger and makes the prop unnecessary.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe step of the menu button — and of it alone: everything else in the header belongs to the consumer, and the height of the row is set by `--gr-navbar-height`. The fallback is `sm` (32px) rather than the `md` shared by the package: the button has been that size from the very beginning, and raising the default would shift the header for everyone. If a touch target is needed, `size="lg"` gives 44px, exactly what WCAG 2.5.5 and the Apple HIG require; it fits into a row of 56px.
showMenuButtonboolean | undefinedfalse
menuButtonClassstring | undefined""Extra classes applied to the menu button wrapper (e.g. `sm:hidden`).
stickyboolean | undefinedfalseThe bar sticks to the top while scrolling. The layer is `--gr-z-navbar`: it is below the anchored panels, so that an open list does not slide under the header.

Slots

SlotTypeDescription
titleanyThe heading as a whole — instead of the `title` string.
leftanyThe zone right after the heading: tabs, a section switch.
centeranyThe central zone: search, breadcrumbs.
defaultanyThe right zone: actions, the avatar.

Events

EventTypeDescription
menu[]

Examples 3

Actions slot in page shell

Acme Console

Правый слот — для глобальных действий уровня приложения: поиск, уведомления, аккаунт. Для навигации по разделам используйте GrSidebar (десктоп) или GrBottomNav (мобайл), а не сам хедер.

Actions
<script setup lang="ts">
import { ref } from 'vue'

import { GrAvatar, GrBadge, GrButton, GrNavbar } from '@feugene/granularity'

const notifications = ref(3)
</script>

<template>
  <!--
    GrNavbar — это ГЛОБАЛЬНЫЙ верхний хедер приложения (`<header>`-landmark),
    который тянется на всю ширину над маршрутизируемым контентом. Здесь он показан
    в контексте настоящего app-shell: хедер сверху + контент под ним.
    Не путайте с секционными заголовками страниц и marketing-навигацией.
  -->
  <div class="overflow-hidden rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] shadow-[var(--gr-shadow-1)]">
    <GrNavbar title="Acme Console">
      <GrButton size="sm" variant="ghost" square aria-label="Search">
        <span class="i-lucide-search block h-4 w-4" aria-hidden="true" />
      </GrButton>

      <GrButton size="sm" variant="ghost" square aria-label="Notifications" @click="notifications = 0">
        <span class="relative inline-flex">
          <span class="i-lucide-bell block h-4 w-4" aria-hidden="true" />
          <GrBadge v-if="notifications" size="xs" tone="danger" dark class="absolute -right-2 -top-2">
            {{ notifications }}
          </GrBadge>
        </span>
      </GrButton>

      <GrAvatar :size="28" alt="Ada Lovelace" />
    </GrNavbar>

    <!-- Условный контент приложения под хедером -->
    <div class="grid gap-3 p-5">
      <div class="h-3 w-40 rounded bg-[var(--gr-muted)]" />
      <div class="grid gap-2 sm:grid-cols-3">
        <div class="h-16 rounded-lg border border-[var(--gr-brd)] bg-[var(--gr-card)]" />
        <div class="h-16 rounded-lg border border-[var(--gr-brd)] bg-[var(--gr-card)]" />
        <div class="h-16 rounded-lg border border-[var(--gr-brd)] bg-[var(--gr-card)]" />
      </div>
    </div>
  </div>

  <p class="mt-3 text-sm text-[var(--gr-muted-fg)]">
    Правый слот — для глобальных действий уровня приложения: поиск, уведомления, аккаунт.
    Для навигации по разделам используйте <code>GrSidebar</code> (десктоп) или
    <code>GrBottomNav</code> (мобайл), а не сам хедер.
  </p>
</template>

Responsive menu trigger

Mobile shell
Tap the hamburger to open the navigation drawer.

Menu Toggle
<script setup lang="ts">
import { ref } from 'vue'

import { GrNavbar } from '@feugene/granularity'

const isMenuOpen = ref(false)
const navItems = ['Overview', 'Deployments', 'Billing', 'Settings']
</script>

<template>
  <!--
    Граница применения: кнопка-гамбургер (`show-menu-button`) нужна ТОЛЬКО в
    компактном/мобильном режиме, когда постоянный `GrSidebar` скрыт. Событие `menu`
    открывает off-canvas панель навигации. На десктопе кнопку прячут (`sm:hidden`),
    а разделы живут в боковой панели.
  -->
  <div class="relative overflow-hidden rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] shadow-[var(--gr-shadow-1)]">
    <GrNavbar
      title="Mobile shell"
      show-menu-button
      @menu="isMenuOpen = !isMenuOpen"
    />

    <div class="relative min-h-[160px]">
      <!-- Off-canvas панель навигации, которую открывает кнопка меню -->
      <transition
        enter-active-class="transition-transform duration-200 ease-out"
        enter-from-class="-translate-x-full"
        leave-active-class="transition-transform duration-150 ease-in"
        leave-to-class="-translate-x-full"
      >
        <nav
          v-if="isMenuOpen"
          class="absolute inset-y-0 left-0 z-10 w-52 border-r border-[var(--gr-brd)] bg-[var(--gr-card)] p-3"
        >
          <button
            v-for="item in navItems"
            :key="item"
            type="button"
            class="block w-full rounded-lg px-3 py-2 text-left text-sm text-[var(--gr-fg)] transition-colors hover:bg-[var(--gr-muted)]"
            @click="isMenuOpen = false"
          >
            {{ item }}
          </button>
        </nav>
      </transition>

      <div class="p-5 text-sm text-[var(--gr-muted-fg)]">
        {{ isMenuOpen ? 'Navigation drawer is open — pick a section.' : 'Tap the hamburger to open the navigation drawer.' }}
      </div>
    </div>
  </div>
</template>

Custom title slot

Release dashboard Beta
Три зоны хедера: слева #title (брендинг, статус-бейдж) и #left, по центру #center — поиск или хлебные крошки, справа слот по умолчанию. Проп title не нужен, когда заголовок собран разметкой. С sticky панель остаётся сверху при прокрутке, но не перекрывает выпадающие панели.

Title Slot
<script setup lang="ts">
import { GrBadge, GrButton, GrInput, GrNavbar } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-3 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-3">
    <GrNavbar sticky>
      <template #title>
        <div class="flex items-center gap-2">
          <span class="text-sm font-semibold">Release dashboard</span>
          <GrBadge size="sm" tone="info">
            Beta
          </GrBadge>
        </div>
      </template>

      <template #center>
        <GrInput placeholder="Search releases" size="sm" aria-label="Search releases" />
      </template>

      <GrButton size="sm">
        Publish
      </GrButton>
    </GrNavbar>

    <div class="px-4 py-3 text-sm text-[var(--gr-muted-fg)]">
      Три зоны хедера: слева <code>#title</code> (брендинг, статус-бейдж) и <code>#left</code>,
      по центру <code>#center</code> — поиск или хлебные крошки, справа слот по умолчанию.
      Проп <code>title</code> не нужен, когда заголовок собран разметкой. С <code>sticky</code>
      панель остаётся сверху при прокрутке, но не перекрывает выпадающие панели.
    </div>
  </div>
</template>

Component documentationAll components