componente · @solu30/ui-kit/navigation
MobileBottomNav
Barra de navegación fija al pie en móvil, con badges y un botón de más que abre un panel.
Seguir leyendo — 3 párrafos más
MobileBottomNav renders a fixed iOS/Android-style bottom navigation bar.
Features:
- Fixed to viewport bottom with safe-area inset support
- Hidden on md+ breakpoints (mobile-only)
- Active pill background on selected item
- Badge support (any React node)
- "More" overflow slot with Sheet bottom overlay
- renderLink for framework-agnostic SPA navigation (Next.js Link, etc.)
- Touch-friendly targets (min 48px)
- motion-safe:active:scale-[0.96] press feedback
- WCAG 2.1: role="navigation", aria-current="page" on active items
- Adaptive layout by tab count (1-4 flex | 5 compact text | 6+ horizontal scroll)
- Scroll gradient chevron indicators when tabs overflow
- animatedIndicator prop: barra superior que se desliza entre tabs, en CSS
- currentPath prop: URL-based active state from href matching
- leadingAction prop: leading ReactNode slot (back button, menu toggle, etc.)
Theming via CSS variables (resolved by each vertical's theme):
- --ui-bg-raised / bg-container: nav background
- --ui-border: top border color
- --ui-accent / text-primary: active label + pill color
- --ui-content-muted / text-content/60: inactive label
4 sitios de import en 3 de 15 verticales — un archivo que la importa cuenta una vez, monte una instancia o veinte.
- carlosmartinpavon
- gestorsalon
- raquetea
La pieza, con lugar de pantalla
Cargando el espécimen…
export function MobileBottomNavSpecimen() {
const [ruta, setRuta] = useState('/app/muro')
const items = [
{ id: '/app/muro', label: 'Muro', icon: IconChat, href: '/app/muro' },
{ id: '/app/reservar', label: 'Reservar', icon: IconClipboard, href: '/app/reservar' },
{ id: '/app/competir', label: 'Competir', icon: IconTarget, href: '/app/competir' },
{ id: '/app/cuenta', label: 'Cuenta', icon: IconUser, href: '/app/cuenta', badge: 2 },
{ id: 'menu', label: 'Menú', icon: IconMenu, onClick: () => {} },
]
return (
<Stage label="MobileBottomNav · la barra inferior del panel del socio de Raquetea — activo por currentPath" tall>
<div className="flex flex-col items-center gap-2 px-1 py-1">
<PhoneFrame className="w-[240px]">
<div className="flex h-[340px] flex-col overflow-hidden bg-bkg">
<div className="flex-1 space-y-2 p-3">
<div className="h-3 w-28 rounded bg-container" />
<div className="h-16 rounded-lg bg-container" />
<div className="h-16 rounded-lg bg-container" />
<div className="h-16 rounded-lg bg-container" />
</div>
<MobileBottomNav
items={items}
currentPath={ruta}
// El vertical pasa un `next/link`. Acá un `<span>` clickeable:
// los href son de Raquetea, no rutas de este sitio.
renderLink={({ href, className, children }) => (
<span
role="link"
tabIndex={0}
className={className}
onClick={() => setRuta(href)}
onKeyDown={(e) => e.key === 'Enter' && setRuta(href)}
>
{children}
</span>
)}
className="!static md:!flex"
/>
</div>
</PhoneFrame>
<p className="max-w-[380px] text-center text-[11px] text-content/60">
El activo no se pasa por ítem: sale de comparar `currentPath` contra cada `href`. El
quinto botón no navega — abre el menú lateral.
</p>
</div>
</Stage>
)
}De specimens/navigation.tsx · MobileBottomNavSpecimen: el espécimen del Muestrario, no un ejemplo oficial del paquete. Muestra cómo se usa la pieza de verdad, montada contra los tipos instalados —eso es lo que vale— pero no es documentación del kit.
Cómo se usa
import { MobileBottomNav } from '@solu30/ui-kit/navigation'Props
| Prop | Requerida | Tipo | Qué hace |
|---|---|---|---|
| items | sí | MobileBottomNavItem[] | Primary navigation items |
| moreLabel | opcional | string | Label for the "More" overflow button. If omitted, no more button is shown. |
| moreActive | opcional | boolean | Whether the "More" button should appear active |
| moreContent | opcional | React__default.ReactNode | Content rendered inside the "More" bottom sheet (vertical-specific) |
| moreTitle | opcional | string | Title for the more sheet |
| renderLink | opcional | (props: { href: string; className: string; children: React__default.ReactNode; 'aria-current'?: 'page'; }) => React__default.ReactNode | Custom link renderer for framework integration (Next.js Link, etc.) |
| currentPath | opcional | string | Current URL path for auto-detecting active state via href matching. When provided, a tab with `href` is considered active if `currentPath === href` or `currentPath.startsWith(href + '/')`. Explicit `item.active` takes precedence over path matching. |
| leadingAction | opcional | React__default.ReactNode | Leading action slot — rendered to the left of the tab strip, separated by a vertical divider (e.g., back button or menu toggle). |
| animatedIndicator | opcional | boolean | When `true`, renders a top-bar indicator that slides between active tabs. ADR-341 — era un `layoutId` de framer-motion cargado con un `require()` dinámico para que el bundle se pudiera tree-shakear cuando la prop no se usaba. Ahora es CSS, así que no hay nada que tree-shakear ni peer que resolver: la prop sigue existiendo porque sigue siendo una decisión de diseño —hay barras de navegación donde la marca que viaja distrae— y no porque cueste algo tenerla prendida. De paso arregla un defecto que el `layoutId` traía: era la constante `"mobileBottomNavActiveIndicator"`, la MISMA para todas las instancias, así que dos navs montadas a la vez se peleaban el mismo indicador. El de ahora es por instancia por construcción. @default false |
| className | opcional | string | — |