DEV Community

Dev Maya
Dev Maya

Posted on

Building a production-ready SaaS dashboard in Next.js 16 — Recharts, TanStack Table, dark mode, and collapsible sidebar

I've been building Next.js templates as a side project. This is the most technical one yet: Pulse, a full SaaS analytics dashboard with 5 pages, Recharts charts, TanStack Table, and a persistent dark/light mode system.

Here are the four implementation details that took the most thought.

1. Dark/light mode with zero flash

CSS custom properties on :root for light mode, overridden on [data-theme="dark"] for dark mode:

:root {
  --bg: #f4f6fb;
  --text-1: #0f172a;
  --accent: #7c3aed;
}

[data-theme="dark"] {
  --bg: #0a0a0f;
  --text-1: #f1f5f9;
  --accent: #8b5cf6;
}
Enter fullscreen mode Exit fullscreen mode

The toggle sets the attribute on html and persists to localStorage:

const toggle = () => {
  const next = theme === 'light' ? 'dark' : 'light'
  document.documentElement.setAttribute('data-theme', next)
  localStorage.setItem('pulse-theme', next)
  setTheme(next)
}
Enter fullscreen mode Exit fullscreen mode

Zero flash because data-theme="light" is set on the html element in layout.tsx. No hydration mismatch, no FOUC.

2. Collapsible sidebar with CSS variables

Two variables control the layout:

:root {
  --sidebar-w: 240px;
  --sidebar-collapsed-w: 68px;
}
Enter fullscreen mode Exit fullscreen mode

The main content uses margin-left that transitions between them:



Enter fullscreen mode Exit fullscreen mode

One state variable, two CSS variables, one CSS transition. Zero JavaScript animation logic.

3. TanStack Table with pre-filtered data

For plan and status dropdowns, I compute the filtered array before passing to useReactTable:

const filtered = useMemo(() => {
  return CUSTOMERS.filter(c =>
    (plan === 'All' || c.plan === plan) &&
    (status === 'All' || c.status === status)
  )
}, [plan, status])

const table = useReactTable({
  data: filtered,
  onGlobalFilterChange: setGlobalFilter, // search still via TanStack
})
Enter fullscreen mode Exit fullscreen mode

Custom UI controls stay decoupled from TanStack internals. Global search goes through TanStack's getFilteredRowModel. The two compose cleanly.

4. Auto-coloring Badge component

const variantMap: Record = {
  Active: 'green', Inactive: 'gray', Trial: 'amber',
  Free: 'gray', Pro: 'blue', Enterprise: 'violet',
  Completed: 'green', Pending: 'amber', Failed: 'red',
}

export function Badge({ children, variant }: BadgeProps) {
  const v = variant ?? variantMap[String(children)] ?? 'gray'
  return {children}
}
Enter fullscreen mode Exit fullscreen mode

Pass any status or plan as children — auto-maps to the right color. Used in 3 tables with zero configuration at call sites.


Live demo: https://pulse-dashboard-demo.vercel.app/dashboard

Available on Gumroad for $99: https://devmaya.gumroad.com/l/pulse

Top comments (0)