The DashboardNavigation component provides a centralized, role-based navigation solution for dashboard pages with autonomous permission checking and extensive customization through slots.

DashboardNavigation extracts navigation logic from page layouts into a standalone, reusable component that handles role-based visibility, permission checking, and custom navigation items through a flexible slot system.

🎯 Autonomous

Self-contained permission checking—no external permission props required.

🔌 Slot-Based

Three customization slots (before, default, after) for flexible navigation layouts.

🛡️ Role-Based

Automatic visibility control based on user roles (admin, team_admin, team_manager).

⚡ Performance

Parallel permission checks with optional pre-computed permissions for layouts.

The component renders these navigation items by default:

ItemURLRequired RoleAlways Visible
Orders/dashboard/ordersteam_admin❌
Events/dashboard/eventsadmin❌
Users/dashboard/usersteam_admin❌
Clients/dashboard/clientsteam_manager❌

The simplest usage requires no props—the component handles all permission checking internally:

---
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
---
<nav>
<DashboardNavigation />
</nav>
Section titled “With Pre-Computed Permissions (Recommended for Layouts)”

For better performance in layouts where permissions are already computed, pass them to avoid redundant checks:

---
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
import { canViewContent } from '#utils/role-guard'
// Compute permissions once
const [canEditUsers, canEditClients, canEditProducts] = await Promise.all([
canViewContent(Astro.locals, ['team_admin']),
canViewContent(Astro.locals, ['team_manager']),
canViewContent(Astro.locals, ['team_admin', 'admin']),
])
---
<DashboardNavigation
permissions={{
canEditUsers,
canEditClients,
canEditProducts,
}}
/>

Use the hideItems prop to hide specific navigation items:

<DashboardNavigation hideItems={['events', 'users']} />

This would show only Orders and Clients (if user has access).

The component provides three slots for customization:

Add items before the default navigation:

<DashboardNavigation>
<li slot="before">
<Link.LinkButton href="/dashboard/home">
<Icon name="home" /> Home
</Link.LinkButton>
</li>
</DashboardNavigation>

Add items after the default navigation:

<DashboardNavigation>
<li slot="after">
<Link.LinkButton href="/dashboard/settings">
<Icon name="settings" /> Settings
</Link.LinkButton>
</li>
</DashboardNavigation>

Replace all default navigation items with custom ones:

<DashboardNavigation hideDefaults={true}>
<li>
<Link.LinkButton href="/custom-dashboard">Custom</Link.LinkButton>
</li>
<li>
<Link.LinkButton href="/reports">Reports</Link.LinkButton>
</li>
</DashboardNavigation>

You can combine multiple slots for maximum flexibility:

<DashboardNavigation hideItems={['users']}>
<li slot="before">
<Link.LinkButton href="/dashboard/overview">Overview</Link.LinkButton>
</li>
<!-- Default items (Orders, Events, Clients) render here -->
<li slot="after">
<Link.LinkButton href="/dashboard/analytics">Analytics</Link.LinkButton>
</li>
<li slot="after">
<Link.LinkButton href="/dashboard/settings">Settings</Link.LinkButton>
</li>
</DashboardNavigation>
type Props = {
hideDefaults?: boolean
hideItems?: ('orders' | 'events' | 'users' | 'clients' | 'products')[]
permissions?: {
canEditUsers?: boolean
canEditClients?: boolean
canEditProducts?: boolean
}
className?: string
debug?: boolean
}
  • Type: boolean
  • Default: false
  • Description: Hide all default navigation items. Use with default slot for fully custom navigation.
  • Type: Array<'orders' | 'events' | 'users' | 'clients'>
  • Default: []
  • Description: Array of specific items to hide from default navigation.
  • Type: Object

  • Default: undefined

  • Description: Pre-computed permission checks. If not provided, component computes them internally.

    • canEditUsers: User can view Users (team_admin role)
    • canEditClients: User can view Clients (team_manager role)
    • canEditProducts: User can view Products (team_admin or admin role)

    Events is not in this list: /dashboard/events only redirects signed-out visitors, so its link is shown to every signed-in user rather than gated.

  • Type: string
  • Default: ""
  • Description: Additional CSS classes for the nav element.
  • Type: boolean
  • Default: false
  • Description: Enable console logging for permission checks (useful during development).
SlotDescriptionExample Use Case
beforeContent before default itemsAdd “Home” or “Overview” link
defaultReplace all navigationFully custom navigation
afterContent after default itemsAdd “Settings” or “Profile” link
src/pages/dashboard/admin.astro
---
import { Auth } from '#layouts'
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
import { Link } from '@fpkit/acss'
---
<Auth pageTitle="Admin Dashboard" requiredRoles={['admin']}>
<nav>
<DashboardNavigation>
<li slot="before">
<Link.LinkButton href="/dashboard">Dashboard Home</Link.LinkButton>
</li>
<li slot="after">
<Link.LinkButton href="/dashboard/reports">Reports</Link.LinkButton>
</li>
<li slot="after">
<Link.LinkButton href="/dashboard/system">System Settings</Link.LinkButton>
</li>
</DashboardNavigation>
</nav>
</Auth>
src/pages/client-portal.astro
---
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
import { Link } from '@fpkit/acss'
---
<DashboardNavigation hideDefaults={true}>
<li>
<Link.LinkButton href="/portal/overview">Overview</Link.LinkButton>
</li>
<li>
<Link.LinkButton href="/portal/orders">My Orders</Link.LinkButton>
</li>
<li>
<Link.LinkButton href="/portal/support">Support</Link.LinkButton>
</li>
</DashboardNavigation>
src/layouts/TeamLayout.astro
---
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
import { canViewContent } from '#utils/role-guard'
// Check if user is team manager
const isTeamManager = await canViewContent(Astro.locals, ['team_manager'])
---
<DashboardNavigation hideItems={isTeamManager ? [] : ['clients']}>
<li slot="after">
<Link.LinkButton href="/dashboard/team">Team</Link.LinkButton>
</li>
</DashboardNavigation>

Enable debug mode to see permission check results in the console:

<DashboardNavigation debug={true} />

Console output:

DashboardNavigation - Computed permissions: {
canEditUsers: false,
canEditClients: true,
canEditProducts: true
}

When using the component in layouts where permissions are already checked (e.g., Auth layout), pass pre-computed permissions to avoid redundant database queries:

❌ Not Recommended (redundant checks):

---
const canEditUsers = await canViewContent(Astro.locals, ['team_admin'])
const canEditClients = await canViewContent(Astro.locals, ['team_manager'])
const canEditEvents = await canViewContent(Astro.locals, ['admin'])
---
<!-- Component will re-check permissions internally -->
<DashboardNavigation />

✅ Recommended (efficient):

---
// Compute once, reuse everywhere
const [canEditUsers, canEditClients, canEditProducts] = await Promise.all([
canViewContent(Astro.locals, ['team_admin']),
canViewContent(Astro.locals, ['team_manager']),
canViewContent(Astro.locals, ['team_admin', 'admin']),
])
---
<!-- Pass pre-computed permissions -->
<DashboardNavigation
permissions={{
canEditUsers,
canEditClients,
canEditProducts,
}}
/>

The component uses Promise.all() internally for parallel permission checks, reducing latency by ~66% compared to sequential checks.

The DashboardNavigation component follows a clear separation of concerns:

  • Component Responsibility: Navigation rendering and role-based visibility
  • Layout Responsibility: Page-level access control (requiredRoles guard)
  • Parent Flexibility: Optional pre-computed permissions for performance

The Auth layout uses DashboardNavigation for consistent navigation across protected pages:

src/layouts/Auth.astro
---
import DashboardNavigation from '#components/astro/DashboardNavigation.astro'
---
<section class="flex">
<DashboardNavigation />
</section>

The layout handles page-level protection, while the component handles navigation visibility.

Auth Layout Role Guards

Learn about declarative page protection using Auth layout props.

View Guide

Configurable Roles

Understand the role system and permission hierarchy.

View Guide

@fpkit/acss Components

Explore the Nav and Link components used internally.

View Documentation

If you’re migrating from the original Auth layout navigation code:

---
const [canEditUsers, canEditClients, canEditEvents] = await Promise.all([
canViewContent(Astro.locals, ['team_admin']),
canViewContent(Astro.locals, ['team_manager']),
canViewContent(Astro.locals, ['admin']),
])
---
<Nav>
<ul>
<li>
<Link.LinkButton href="/dashboard/orders">Orders</Link.LinkButton>
</li>
{canEditEvents && (
<li>
<Link.LinkButton href="/dashboard/events">Events</Link.LinkButton>
</li>
)}
{canEditUsers && (
<li>
<Link.LinkButton href="/dashboard/users">Users</Link.LinkButton>
</li>
)}
{canEditClients && (
<li>
<Link.LinkButton href="/dashboard/clients">Clients</Link.LinkButton>
</li>
)}
</ul>
</Nav>

Benefits:

  • 40+ lines reduced to 1 line
  • No permission checks needed
  • Reusable across pages
  • Consistent navigation everywhere
  1. Use in Layouts: Import once in your layout component for consistent navigation
  2. Pre-Compute When Possible: Pass permissions prop when already computed for better performance
  3. Slot Composition: Use slots for page-specific navigation rather than forking the component
  4. Hide Strategically: Use hideItems to conditionally show/hide based on context
  5. Debug in Development: Enable debug={true} to troubleshoot permission issues

Problem: Expected navigation items don’t appear

Solutions:

  1. Check user roles in the Auth layout with <UserInfo showRoleBadge />
  2. Enable debug mode: <DashboardNavigation debug={true} />
  3. Verify role configuration in config/roles.config.ts

Problem: Slow navigation rendering

Solutions:

  1. Pass pre-computed permissions from layout to avoid redundant checks
  2. Ensure parallel permission checks are working (check network tab)
  3. Consider caching permission results in middleware

Problem: Slot content doesn’t appear

Solutions:

  1. Ensure slot names are correct (before, after, or no name for default)
  2. When using default slot, set hideDefaults={true}
  3. Wrap slot content in proper HTML elements (<li> for list items)

Need help? Check the Auth Layout Role Guards guide or review the component source code.