DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding a Mobile‑First TabBar to the Albaida‑Tickets Dashboard (React + Next.js)

Adding a Mobile‑First TabBar to the Albaida‑Tickets Dashboard (React + Next.js)

TL;DR: I added a client‑side TabBar.tsx component to make the Albaida‑Tickets admin UI usable on phones, wiring it to Next.js routing and styling it with Tailwind. The change lives in components/TabBar.tsx and is referenced from pages/dashboard.tsx, fixing the “content overflows on mobile” symptom.


The Problem

The Albaida‑Tickets dashboard was built as a desktop‑only grid of cards. When I opened the page on an iPhone X, the layout overflowed horizontally, the filter sidebar never collapsed, and the bottom navigation was missing. The console showed no errors, but the UI was unusable:

Viewport width: 375px
Dashboard container width: 1024px → horizontal scroll
Enter fullscreen mode Exit fullscreen mode

Because the app is a Next.js project with server‑rendered pages, I couldn’t just drop a CSS media query on the existing layout; the navigation needed a mobile‑specific component that works on the client (uses next/link and useRouter).


What I Tried First

My first attempt was to hide the existing sidebar with a simple Tailwind utility (hidden md:block) and rely on the top navigation for navigation. That approach failed for two reasons:

  1. No persistent navigation: Users lost quick access to the main sections (Tickets, Analytics, Settings).
  2. Route detection broke: The active state was tied to the server‑side layout, so the highlighted link never updated after a client‑side navigation.

I also tried injecting a pure CSS “bottom bar” via a global stylesheet, but because the component needed to read the current route to apply the active style, the static CSS couldn’t react to client navigation.


The Implementation

1. Create the TabBar component

File: components/TabBar.tsx

import Link from 'next/link';
import { useRouter } from 'next/router';
import clsx from 'clsx';

type Tab = {
  href: string;
  label: string;
  icon: React.ReactNode;
};

const tabs: Tab[] = [
  {
    href: '/dashboard',
    label: 'Tickets',
    icon: (
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path d="M3 7h18M3 12h18M3 17h18" />
      </svg>
    ),
  },
  {
    href: '/analytics',
    label: 'Analytics',
    icon: (
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path d="M12 20V10M18 20V4M6 20v-8" />
      </svg>
    ),
  },
  {
    href: '/settings',
    label: 'Settings',
    icon: (
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
        <path d="M19.4 15a1.65 1.65 0 01.33 1.82l-1 1.73a1.65 1.65 0 01-1.9.81l-2-.5a1.65 1.65 0 01-1.1-.9l-.6-1.1a7.07 7.07 0 01-2.4 0l-.6 1.1a1.65 1.65 0 01-1.1.9l-2 .5a1.65 1.65 0 01-1.9-.81l-1-1.73a1.65 1.65 0 01.33-1.82l.9-.7a7.07 7.07 0 010-2.4l-.9-.7a1.65 1.65 0 01-.33-1.82l1-1.73a1.65 1.65 0 011.9-.81l2 .5a1.65 1.65 0 011.1.9l.6 1.1a7.07 7.07 0 012.4 0l.6-1.1a1.65 1.65 0 011.1-.9l2-.5a1.65 1.65 0 011.9.81l1 1.73a1.65 1.65 0 01-.33 1.82l-.9.7a7.07 7.07 0 010 2.4l.9.7z" />
      </svg>
    ),
  },
];

export default function TabBar() {
  const { pathname } = useRouter();

  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 md:hidden">
      <ul className="flex justify-around">
        {tabs.map(({ href, label, icon }) => {
          const isActive = pathname === href;
          return (
            <li key={href}>
              <Link href={href}>
                <a
                  className={clsx(
                    'flex flex-col items-center py-2 text-sm',
                    isActive
                      ? 'text-blue-600'
                      : 'text-gray-600 hover:text-gray-800'
                  )}
                >
                  {icon}
                  <span>{label}</span>
                </a>
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key points

  • Client‑side routing: useRouter gives us the current path, enabling the active style without a full page reload.
  • Tailwind utilities: The component is hidden on md and larger (md:hidden) so it only appears on phones.
  • clsx: Keeps conditional class logic readable.

2. Wire the component into the dashboard layout

File: pages/dashboard.tsx (excerpt)

import DashboardGrid from '@/components/DashboardGrid';
import TabBar from '@/components/TabBar';

export default function DashboardPage() {
  return (
    <>
      <main className="pt-4 pb-20 md:pb-4">
        <DashboardGrid />
      </main>

      {/* Mobile navigation */}
      <TabBar />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

I placed <TabBar /> after the main content so it stays fixed at the bottom. The pb-20 on the <main> element reserves space for the bar on mobile.

3. Update the changelog and metadata

The weekly automation script adds a new entry to content/2026/10/24/albaida-tickets/changelog.md:

## [2026-10-24] albaida-tickets

### Added
- `components/TabBar.tsx` – new bottom navigation for mobile devices (client component with Link, route detection and Tailwind styling)
Enter fullscreen mode Exit fullscreen mode

And flips the generation flags in content/2026/10/24/albaida-tickets/metadata.json:

-  "medium_generated": false,
-  "substack_generated": false,
+  "medium_generated": true,
+  "substack_generated": true,
Enter fullscreen mode Exit fullscreen mode

These changes are consumed by the content‑automation pipeline that builds the weekly newsletter and the Dev.to post you’re reading now.

4. Verify the fix

Running the dev server (npm run dev) and opening the page on a Chrome device emulator showed:

  • No horizontal scroll.
  • The TabBar appears at the bottom, icons highlight correctly when navigating.
  • The desktop layout remains untouched (md breakpoint).

I also added a quick Cypress test (`c


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-10-25

#playadev #buildinpublic

Top comments (0)