Skip to content

BottomNav ​

The mobile tab bar: a fixed, lg:hidden bar pinned to the bottom of the viewport with labels, icons and per-item badges. Tabs either navigate (to) or emit (action), and the app owns the item list.

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

<template>
  <RyderBottomNav :items="items" @select="onSelect" />
</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 BottomNav from "../../../src/components/navigation/BottomNav.vue";

// The kit's nav reads `useRoute()` from `#imports` (vue-router). The docs host
// has no router, so the active-item check would run against `undefined` here;
// supply a path only when nothing else has. A Nuxt app's router provides the
// real route and this is a no-op.
if (!inject(routeLocationKey, null)) {
  provide(routeLocationKey, { path: "/" } as RouteLocationNormalizedLoaded);
}

const items = [
  {
    label: "Home",
    to: "/",
    icon: "ri:home-line",
    activeIcon: "ri:home-fill",
  },
  { label: "Search", icon: "heroicons:magnifying-glass", action: "open-search" },
  { label: "Cart", icon: "heroicons:shopping-cart", action: "toggle-cart" },
];

const cartCount = ref(2);
const lastSelect = ref("");

function onSelect(item: { label: string; action?: string }) {
  lastSelect.value = item.action
    ? `${item.label} → ${item.action}`
    : item.label;
}
</script>

<template>
  <div class="space-y-3">
    <p class="text-xs text-gray-400 dark:text-white/30">
      The bar is fixed to the viewport bottom and hidden from 1024px up — narrow
      the window to see it. An <code class="font-mono">action</code> item only
      emits <code class="font-mono">select</code>; the Home item navigates.
      Last select: {{ lastSelect || "—" }}
    </p>

    <BottomNav :items="items" @select="onSelect">
      <template #badge="{ item, active }">
        <span
          v-if="item.action === 'toggle-cart' && cartCount > 0"
          class="absolute -right-2.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-bold text-white"
          :class="active ? 'bg-primary-600' : 'bg-red-500'"
        >
          {{ cartCount }}
        </span>
      </template>
    </BottomNav>
  </div>
</template>

Props ​

PropTypeDefaultDescription
itemsBottomNavItem[][]Tab entries: { label, to?, icon, activeIcon?, action? }.
bgClassstring"bg-white dark:bg-onyx-600"Classes on the konsta tabbar background.

BottomNavItem fields:

FieldTypeDescription
labelstringTab label (also the key).
tostring?Route to navigate to; omit when action is set.
iconstringIcon shown when the item is inactive.
activeIconstring?Swap in while the item is active (filled variant, e.g.).
actionstring?Non-navigation item — emitted through select instead of being routed.

Emits ​

EventPayloadWhen
selectitem: BottomNavItemAn action item was tapped. Never fires for to items (those navigate).

Slots ​

SlotPropsContent
badge{ item, active }Pill rendered inside the icon wrapper — typically a count on an action item.

Notes ​

  • The bar is fixed inset-x-0 bottom-0 z-40 lg:hidden: it is a viewport-level element, hidden from 1024px up — narrow the window (or use devtools device mode) to see it in a docs page.
  • Active state: route.path === item.to, plus `route.path.startsWith(item.to
    • "/") for sub-paths (to: "/"matches exactly only). Items withaction(or withoutto`) are never active.
  • Nuxt-only imports: navigateTo, useRoute, useColorMode come from #imports. They are available automatically in a Nuxt app; the docs host shims them (the nav then navigates by setting location.href), which is why the example stubs vue-router's route key before rendering the bar.