Conditional Slot Rendering for Dashboards and Tabs
Drive dashboard sections and tabbed views by mapping route segments to parallel slots.
Conditional Slot Rendering for Dashboards and Tabs is a free Next.js 15 Fullstack (App Router + Server Actions) lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Next.js 15 Fullstack (App Router + Server Actions) learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Are Parallel Routes?
Next.js 15 introduces Parallel Routes, a layout primitive that lets you render multiple pages simultaneously within the same layout — each in its own named slot.
This is ideal for dashboards where you want independent sections like an analytics panel, a team feed, and a notifications sidebar all loading concurrently and independently.
- Slots are defined with the
@slotNamefolder convention inside a layout segment - Each slot is passed as a prop to the parent
layout.tsx - Slots render in parallel — one can stream while another is already resolved
Think of slots as named holes in your layout that the router fills with different route subtrees simultaneously.
Folder Structure for Parallel Slots
To create parallel slots, place @slotName folders directly inside your route segment folder. Each slot folder acts as its own mini route tree.
A typical dashboard might look like this:
app/dashboard/layout.tsx— receives all slot propsapp/dashboard/@analytics/page.tsx— analytics slotapp/dashboard/@team/page.tsx— team feed slotapp/dashboard/@notifications/page.tsx— notifications slot
The slot folders do not affect the URL. The URL remains /dashboard while all three slots render simultaneously.
// File: app/dashboard/layout.tsx
// TypeScript interface showing slot props
interface DashboardLayoutProps {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
notifications: React.ReactNode;
}
export default function DashboardLayout({
children,
analytics,
team,
notifications,
}: DashboardLayoutProps) {
return (
<div className="dashboard-grid">
<main className="col-span-2">{children}</main>
<aside className="analytics-panel">{analytics}</aside>
<aside className="team-panel">{team}</aside>
<aside className="notifications-panel">{notifications}</aside>
</div>
);
}The Default File: Handling Unmatched Slots
When you navigate to a URL that does not match a slot's sub-route, Next.js needs to know what to render in that slot. Without a fallback, the entire layout would fail with a 404.
The solution is the default.tsx file inside each slot folder. It acts as a fallback render when the slot has no active match for the current URL.
- Place a
default.tsxin every@slotNamefolder - It can return
nullto render nothing, or a skeleton/placeholder - This keeps other slots functioning even when one has no active sub-route
// File: app/dashboard/@notifications/default.tsx
// Renders nothing when this slot has no active route match
export default function NotificationsDefault() {
return null;
}
// File: app/dashboard/@analytics/default.tsx
// Could render a skeleton instead
export default function AnalyticsDefault() {
return (
<div className="animate-pulse bg-gray-100 rounded-lg h-48" />
);
}Conditional Slot Rendering with Server Components
The layout component receives all slots as props — which means you can apply conditional logic directly in the layout to show or hide slots based on server-side data, user roles, or feature flags.
Because layout.tsx is a Server Component by default, you can perform async data fetching right inside it to make this decision.
- Fetch the current user's role from your database or session
- Conditionally include or exclude slot props from the rendered output
- No client-side JavaScript required for the visibility gate
// File: app/dashboard/layout.tsx
import { getCurrentUser } from '@/lib/auth';
interface DashboardLayoutProps {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}
export default async function DashboardLayout({
children,
analytics,
team,
}: DashboardLayoutProps) {
const user = await getCurrentUser();
const isAdmin = user?.role === 'admin';
return (
<div className="dashboard-grid">
<main>{children}</main>
{isAdmin && <aside>{analytics}</aside>}
<aside>{team}</aside>
</div>
);
}Building a Tabbed Dashboard with Parallel Routes
Parallel routes become especially powerful for tabbed interfaces. Instead of using client-side state to track the active tab, you map each tab to a URL segment, making the tab state part of the URL.
This approach gives you:
- Shareable URLs — users can bookmark a specific tab
- Browser back/forward navigation between tabs
- Independent loading states per tab via Suspense
- Server-rendered tab content with no hydration cost for the tab logic
The tab bar becomes a set of <Link> components pointing to different sub-routes within the slot.
// File: app/dashboard/@tabs/layout.tsx
// Shared tab navigation rendered inside the @tabs slot
import Link from 'next/link';
interface TabsLayoutProps {
children: React.ReactNode;
}
export default function TabsLayout({ children }: TabsLayoutProps) {
return (
<div>
<nav className="flex gap-2 border-b mb-4">
<Link
href="/dashboard/overview"
className="tab-link"
>
Overview
</Link>
<Link
href="/dashboard/revenue"
className="tab-link"
>
Revenue
</Link>
<Link
href="/dashboard/users"
className="tab-link"
>
Users
</Link>
</nav>
<div className="tab-content">{children}</div>
</div>
);
}Mapping Route Segments to Tab Pages
Each tab corresponds to a page.tsx file nested within the @tabs slot. The URL segment becomes the active-tab selector automatically — no JavaScript state needed.
For a dashboard at /dashboard with an @tabs slot:
app/dashboard/@tabs/overview/page.tsx— active at/dashboard/overviewapp/dashboard/@tabs/revenue/page.tsx— active at/dashboard/revenueapp/dashboard/@tabs/users/page.tsx— active at/dashboard/users
The parent layout.tsx URL stays the same (/dashboard/*) while only the @tabs slot updates its content.
// File: app/dashboard/@tabs/revenue/page.tsx
import { getRevenueData } from '@/lib/data';
export default async function RevenueTab() {
const data = await getRevenueData();
return (
<section>
<h2 className="text-xl font-semibold mb-4">Revenue Overview</h2>
<dl className="grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-gray-500">Monthly Recurring</dt>
<dd className="text-2xl font-bold">
${data.mrr.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-sm text-gray-500">Annual Run Rate</dt>
<dd className="text-2xl font-bold">
${data.arr.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-sm text-gray-500">Churn Rate</dt>
<dd className="text-2xl font-bold">{data.churnRate}%</dd>
</div>
</dl>
</section>
);
}Highlighting the Active Tab with usePathname
To visually indicate which tab is active, you need to compare the current URL against each tab's href. The usePathname() hook from next/navigation gives you the current path on the client.
Since the tab bar needs to be interactive (reading the current URL), it must be a Client Component. You can keep only the navigation bar as a Client Component while leaving all tab content as Server Components.
- Mark the tab navigation file with
'use client' - Use
usePathname()to detect the active segment - Apply conditional classes based on the match
'use client';
// File: app/dashboard/@tabs/_components/TabNav.tsx
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const tabs = [
{ href: '/dashboard/overview', label: 'Overview' },
{ href: '/dashboard/revenue', label: 'Revenue' },
{ href: '/dashboard/users', label: 'Users' },
];
export function TabNav() {
const pathname = usePathname();
return (
<nav className="flex gap-1 border-b">
{tabs.map((tab) => {
const isActive = pathname === tab.href;
return (
<Link
key={tab.href}
href={tab.href}
className={`px-4 py-2 text-sm font-medium ${
isActive
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</Link>
);
})}
</nav>
);
}Using Suspense for Independent Slot Loading
One of the biggest advantages of parallel slots is that each slot streams independently. You can wrap individual slots in <Suspense> boundaries so that a slow slot does not block a fast one from rendering.
This eliminates the classic dashboard problem where one slow API call freezes the entire page.
- Wrap each slot in its own
<Suspense>in the layout - Each slot shows its own loading skeleton while it resolves
- Fast slots appear immediately; slow slots stream in when ready
- No
loading.tsxis needed — Suspense gives you fine-grained control
// File: app/dashboard/layout.tsx
import { Suspense } from 'react';
import { AnalyticsSkeleton } from '@/components/skeletons';
interface DashboardLayoutProps {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
notifications: React.ReactNode;
}
export default function DashboardLayout({
children,
analytics,
team,
notifications,
}: DashboardLayoutProps) {
return (
<div className="grid grid-cols-3 gap-4">
<main className="col-span-2">
<Suspense fallback={<p>Loading main...</p>}>
{children}
</Suspense>
</main>
<div className="space-y-4">
<Suspense fallback={<AnalyticsSkeleton />}>
{analytics}
</Suspense>
<Suspense fallback={<p>Loading team...</p>}>
{team}
</Suspense>
<Suspense fallback={null}>
{notifications}
</Suspense>
</div>
</div>
);
}Conditional Slots Based on Search Params
Sometimes you want to control slot visibility with search parameters rather than path segments — for example, toggling a details panel via ?panel=details.
In Server Components, you can access search params via the searchParams prop on page.tsx files, or use useSearchParams() in Client Components.
- Pass
searchParamsdown frompage.tsxto the layout or use a Server Action - In the layout, receive search params via the page's data layer rather than directly (layouts do not receive searchParams)
- A cleaner pattern: use the slot's own
page.tsxto read searchParams and conditionally render content
// File: app/dashboard/@details/page.tsx
// Conditionally renders based on ?panel=details
interface DetailsPageProps {
searchParams: Promise<{ panel?: string }>;
}
export default async function DetailsPanel({
searchParams,
}: DetailsPageProps) {
const { panel } = await searchParams;
if (panel !== 'details') {
return null;
}
return (
<aside className="border-l pl-4">
<h3 className="font-semibold">Details Panel</h3>
<p className="text-sm text-gray-600">
Expanded details visible when ?panel=details is present.
</p>
</aside>
);
}Combining Parallel Routes with Server Actions for Tab State
Server Actions pair naturally with parallel-route tabs. You can use a Server Action to mutate data directly from within a tab's page, then rely on Next.js to revalidate and re-render only the affected slot — not the entire page.
This creates a seamless UX: the user submits a form inside a tab, the action runs on the server, the slot re-streams with fresh data, and all other slots remain untouched.
- Define the Server Action with
'use server'in a separate file or inline - Call
revalidatePathorrevalidateTagto invalidate the data cache - Only the relevant slot re-fetches and re-renders
// File: app/dashboard/@tabs/users/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function deactivateUser(userId: string): Promise<void> {
await db.user.update({
where: { id: userId },
data: { isActive: false },
});
// Only the users tab data is invalidated
revalidatePath('/dashboard/users');
}
// File: app/dashboard/@tabs/users/page.tsx (partial)
// import { deactivateUser } from './actions';
//
// <form action={deactivateUser.bind(null, user.id)}>
// <button type="submit">Deactivate</button>
// </form>Soft Navigation and Slot State Preservation
When navigating between tabs or dashboard sections using Next.js <Link>, the router performs a soft navigation. This means:
- Only the slots whose route segment changed are re-fetched
- Slots with unchanged routes preserve their existing rendered output and React state
- Scroll position in unaffected slots is maintained
This is a key advantage over traditional tab implementations using client-side state — you get URL-driven navigation without the cost of re-rendering the entire page tree on every tab switch.
Hard navigation (full page reload) resets all slots, so always use <Link> for tab transitions inside a parallel-route dashboard.
Knowledge Check: Unmatched Slots
You have a dashboard layout with three parallel slots: @analytics, @team, and @notifications. A user navigates to /dashboard/settings, which is defined under children but has no corresponding sub-route in any of the three slot folders.
What happens to the three slots, and what file prevents a 404 error for the whole layout?
Recap: Conditional Slot Rendering for Dashboards and Tabs
In this lesson you learned how to drive dashboard sections and tabbed views using Next.js 15 Parallel Routes.
Key takeaways:
- Parallel slots use the
@slotNamefolder convention and are passed as props to the parentlayout.tsx - A
default.tsxinside each slot folder is required to handle unmatched URLs and prevent 404 errors - The layout is a Server Component, so you can gate slot rendering with async role checks, feature flags, or any server-side data
- Tab interfaces map each tab to a URL sub-segment within a slot, giving you shareable, bookmarkable, server-rendered tabs with no client state overhead
- Wrap each slot in its own
<Suspense>boundary so slow slots do not block fast ones from streaming - Server Actions inside tab pages can call
revalidatePathto refresh only the affected slot, leaving all other slots untouched - Soft navigation via
<Link>re-fetches only changed slots and preserves React state in unchanged ones
Parallel Routes with conditional slot logic give you a composable, URL-driven architecture for complex dashboards — all without a single piece of tab-management client state.
Frequently asked questions
Is the “Conditional Slot Rendering for Dashboards and Tabs” lesson free?
Yes — the full text of “Conditional Slot Rendering for Dashboards and Tabs” is free to read here on the web, and the Next.js 15 Fullstack (App Router + Server Actions) course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Next.js 15 Fullstack (App Router + Server Actions) course, upgrade to CoddyKit PRO.
What will I learn in “Conditional Slot Rendering for Dashboards and Tabs”?
Drive dashboard sections and tabbed views by mapping route segments to parallel slots. You practise Next.js 15 Fullstack (App Router + Server Actions) with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Next.js 15 Fullstack (App Router + Server Actions)?
No prior experience is required. Next.js 15 Fullstack (App Router + Server Actions) on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Conditional Slot Rendering for Dashboards and Tabs” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Next.js 15 Fullstack (App Router + Server Actions) lesson?
Yes. Every Next.js 15 Fullstack (App Router + Server Actions) lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Parallel Routes with Named Slots and Default Segments
- Intercepting Routes for Shared Modal Experiences
- Conditional Slot Rendering for Dashboards and Tabs
- Building a Photo-Modal Flow with Soft Navigation