Skip to content

SideNav ​

The mobile menu: a left USlideover holding the user header, nav items, a light/dark toggle and a sign-out row. Its open state lives in useMobileNav() rather than in a prop, so the app's hamburger and this drawer stay in sync.

vue
<script setup lang="ts">
import RyderSideNav from "@opeolluwa/ryder/components/navigation/SideNav.vue"
</script>

<template>
  <RyderSideNav :items="items" :user="user" @logout="logout" />
</template>

Examples ​

vue
<script setup lang="ts">
import { inject, provide, ref } from "vue";
import type { RouteLocationNormalizedLoaded } from "vue-router";
import { routeLocationKey } from "vue-router";
import SideNav from "../../../src/components/navigation/SideNav.vue";
import { useMobileNav } from "../../../src/composables/useMobileNav";

// Same `#imports -> vue-router` note as the BottomNav demo: the docs host has
// no router, so give the active-item check a path when nothing else has.
if (!inject(routeLocationKey, null)) {
  provide(routeLocationKey, { path: "/" } as RouteLocationNormalizedLoaded);
}

const { mobileNavOpen, openMobileNav } = useMobileNav();

const items = [
  { label: "Home", to: "/", icon: "heroicons:home" },
  { label: "Cart", icon: "heroicons:shopping-cart", action: "toggle-cart", badge: 3 },
  { label: "Settings", icon: "heroicons:cog-6-tooth", action: "open-settings" },
];

const user = {
  name: "Docs demo",
  description: "Signed in",
  avatar: { text: "DD", alt: "Docs demo" },
};

const lastEvent = ref("");
</script>

<template>
  <div class="space-y-3">
    <div class="flex flex-wrap items-center gap-3">
      <button
        type="button"
        class="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
        @click="openMobileNav()"
      >
        Open side nav
      </button>

      <span class="text-sm text-gray-500 dark:text-white/50">
        {{ lastEvent || `Drawer is ${mobileNavOpen ? "open" : "closed"}` }}
      </span>
    </div>

    <SideNav
      :items="items"
      :user="user"
      root-path="/"
      @action="lastEvent = `action: ${$event}`"
      @logout="lastEvent = 'logout'"
    />
  </div>
</template>

Props ​

PropTypeDefaultDescription
itemsSideNavItem[][]Menu entries: { to?, label, icon, badge?, action? }.
userSideNavUser{}Drawer header identity: { name?, description?, avatar?: { src?, text?, alt?, icon? } }. Missing name falls back to "User".
rootPathstring"/"Path treated as "always active" when it is the current route.

SideNavItem fields:

FieldTypeDescription
tostring?Navigation target; omit for an action item.
labelstringEntry label (and fallback key).
iconstringIcon name.
badgestring | number | null?Trailing count pill (unread count, cart items, …).
actionstring?Non-navigation entry — closes the drawer and emits action with this name.

Emits ​

EventPayloadWhen
actionname: stringAn action item was tapped — item.action ?? item.label, after the drawer closes.
logout—The built-in "Sign out" row was tapped — the app performs its own teardown.

Slots ​

None.

Notes ​

  • Open state: there is no open prop. The drawer binds v-model:open="mobileNavOpen" internally, so the caller opens it through the same composable — useMobileNav() returns mobileNavOpen, openMobileNav(), closeMobileNav() and toggleMobileNav() (backed by Nuxt's useState, so any component in the app sees the same flag).
  • Usually slotted into RyderShell's mobile-nav slot; import it explicitly from app layout files (Nuxt does not auto-import components there).
  • Nuxt-only imports: useColorMode and useRoute from #imports, plus NuxtLink for to items; Nuxt UI provides USlideover, UUser, UButton and USeparator. The docs demo stubs vue-router's route key for the same reason as the BottomNav example (the docs host has no router).
  • The drawer header includes a built-in light/dark toggle that writes colorMode.preference.
  • rootPath only affects active highlighting; items without to render as buttons so a missing route is never requested.