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
| Prop | Type | Default | Description |
|---|---|---|---|
items | BottomNavItem[] | [] | Tab entries: { label, to?, icon, activeIcon?, action? }. |
bgClass | string | "bg-white dark:bg-onyx-600" | Classes on the konsta tabbar background. |
BottomNavItem fields:
| Field | Type | Description |
|---|---|---|
label | string | Tab label (also the key). |
to | string? | Route to navigate to; omit when action is set. |
icon | string | Icon shown when the item is inactive. |
activeIcon | string? | Swap in while the item is active (filled variant, e.g.). |
action | string? | Non-navigation item — emitted through select instead of being routed. |
Emits
| Event | Payload | When |
|---|---|---|
select | item: BottomNavItem | An action item was tapped. Never fires for to items (those navigate). |
Slots
| Slot | Props | Content |
|---|---|---|
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,useColorModecome from#imports. They are available automatically in a Nuxt app; the docs host shims them (the nav then navigates by settinglocation.href), which is why the example stubs vue-router's route key before rendering the bar.