Most websites treat the navbar like a billboard. Same logo. Same links. Same everywhere. But what if your site tells a story? Shouldn't the navbar know which chapter we're in?
I've been building with Nuxt for a while now, and this pattern has become one of my go-to solutions for better UX. Figured it was worth sharing (and hey, what better way to start blogging than with something I actually use in production?).
If you've worked with React or vanilla projects, you know the pain: building different navigation states usually means either cramming tons of conditional logic into one component or creating separate navbar components and trying to keep them in sync. Route-based detection gets messy, auth state management becomes a nightmare, and you end up with navigation logic scattered everywhere.
Nuxt's layout system changes the game entirely.
Why Nuxt Makes This Pattern Shine
Here's where Nuxt really shines compared to other frameworks. In a typical React app, you'd probably:
-
Route-based detection: Check
useLocation()
and conditionally render different navbars - Auth state juggling: Subscribe to auth changes and update navigation accordingly
- Component duplication: Or worse, create separate navbar components that drift apart over time
All of that complexity disappears with Nuxt layouts. Instead of detecting context, you declare it:
<!-- layouts/default.vue - Public pages -->
<template>
<div>
<Navbar type="public" />
<slot />
</div>
</template>
<!-- layouts/auth.vue - Auth flow -->
<template>
<div>
<Navbar type="auth" />
<slot />
</div>
</template>
<!-- layouts/dashboard.vue - Private app -->
<template>
<div>
<Navbar type="private" />
<slot />
</div>
</template>
No route detection. No auth state subscriptions. No complex conditional logic. Each layout simply tells the navbar what context it's in.
The Framework Does the Heavy Lifting
When you navigate from /pricing
(using default.vue
) to /login
(using auth.vue
), Nuxt automatically switches layouts. Your navbar component doesn't need to know anything about routing or authentication - it just responds to the type
prop that each layout provides.
This is declarative UI design at its finest. Instead of "figure out where you are and show the right thing," it becomes "each layout declares what it needs."
Why Context-Aware Navigation Matters
Before we dive into the implementation, let's talk about why this pattern matters for UX:
Cognitive Load Reduction
Users shouldn't see dashboard buttons when they're not logged in. They shouldn't see marketing CTAs when they're already paying customers. Every irrelevant link is mental noise.
Clearer Mental Models
When the navbar adapts to context, it reinforces where users are in your app's journey. Public navbar = marketing mode. Auth navbar = focused mode. Private navbar = work mode.
Better Conversion
A marketing-focused navbar on public pages can improve sign-ups. A task-focused navbar in the app can improve feature adoption. One size doesn't fit all.
The navbar isn't just a static header — it's a narrator that changes tone depending on the scene.
The Design: One Component, Many Faces
Instead of building separate navbars for each context, I use a single component with a type
prop. In my Nuxt app, this gets embedded across different layouts:
<!-- layouts/default.vue -->
<template>
<div>
<Navbar type="public" />
<slot />
</div>
</template>
<!-- layouts/auth.vue -->
<template>
<div>
<Navbar type="auth" />
<slot />
</div>
</template>
<!-- layouts/dashboard.vue -->
<template>
<div>
<Navbar type="private" />
<slot />
</div>
</template>
The magic happens inside the component. Same structure, different content based on which layout is calling it:
<template>
<nav v-if="hydrated" class="navbar-base">
<!-- Logo adapts to context -->
<div v-if="type === 'public' || type === 'private'">
<Logo :darkMode="isDarkMode" />
</div>
<div v-if="type === 'auth'">
<CustomLogo />
</div>
<!-- Navigation content changes completely -->
<div class="nav-actions">
<!-- Public: Login button + language switcher -->
<template v-if="type === 'public'">
<LanguageSwitcher variant="secondary-2" />
<NuxtLink to="/login">
<BaseButton variant="secondary-2">Login</BaseButton>
</NuxtLink>
</template>
<!-- Auth: Minimal, focused -->
<template v-if="type === 'auth'">
<LanguageSwitcher variant="primary-2" />
<BaseButton v-if="authStore.isAuthenticated" @click="handleLogout">
Logout
</BaseButton>
</template>
<!-- Private: Full dashboard navigation -->
<template v-if="type === 'private'">
<DashboardToggle />
<DashboardNav class="hidden lg:block" />
<LanguageSwitcher />
<BaseButton @click="handleLogout" class="hidden lg:block">
Logout
</BaseButton>
<Profile />
<!-- Mobile hamburger menu -->
<MobileNavDropdown />
</template>
</div>
</nav>
</template>
The Technical Reality
This isn't just a nice design pattern - it comes with real technical considerations, especially in Nuxt.
Hydration Mismatch Protection
The v-if="hydrated"
guard prevents that flash of wrong content during SSR:
<script setup>
const hydrated = ref(false);
onMounted(() => {
hydrated.value = true;
});
</script>
Without this, you might see the server-rendered navbar flicker into the client-rendered version. Not a great first impression.
Conditional Styling
Notice how the logo component gets different variants based on context. The auth flow uses CustomLogo
while public/private use the regular Logo
with dark mode detection:
const darkBackgroundRoutes = ['/', '/userType'];
const isDarkMode = computed(() => darkBackgroundRoutes.includes(route.path));
Beyond the Code: Why This Matters
This pattern taught me something important about component design: smart UI doesn't just show what the user can do — it reflects where they are and where they're going.
When you build context-aware components, you're not just reducing code duplication. You're creating interfaces that feel more intuitive because they mirror the user's mental model of your app.
Maintainability
Want to add a new user role? Just extend the type
prop and update the relevant layouts. Need to change the logo across all contexts? Edit one component. The navbar becomes a single source of truth instead of having separate navbar components scattered across your layouts folder.
Layout-Driven Context
Since each layout explicitly passes the type
prop, you get perfect context awareness without complex routing logic. Your default.vue
layout handles public pages, auth.vue
handles authentication flows, and dashboard.vue
handles the private app experience. The navbar just needs to know which layout is calling it.
Extensibility
This pattern scales beautifully. I've extended it to handle admin panels, different user roles, and even A/B testing different navigation structures. The component grows with your app's complexity.
The Bigger Picture
Your navbar might seem like a small detail, but it's one of the few elements users see on every page. Making it context-aware sends a subtle but powerful message: "This app knows where you are and what you need."
Most users won't consciously notice a well-adapted navbar. But they'll definitely feel the difference between an interface that feels smart and one that feels like it's stuck in one gear.
What would your navbar say if it could talk? Mine would say: "Welcome" on the homepage, "Focus" during login, and "Let's work" in the dashboard.
What story is your navbar telling?
Have you implemented context-aware navigation in your apps? What challenges did you face? I'd love to hear about your approach in the comments.
Top comments (0)