The Ultimate Guide to Multi-Theme Layouts in Vue 3 with Vue Router

Published: 2025-12-06
Author: DP
Views: 7
Category: Vue
Content
## Background When developing complex Single Page Applications (SPAs), a common requirement is to support multiple visual themes and page structures within the same project. For instance, a project might include both a back-office admin system with a sidebar and complex navigation, and a public-facing portal with a clean, open layout. Forcing one layout to fit both scenarios leads to bloated and hard-to-maintain code. The most elegant solution is to leverage the power of Vue Router to dynamically load the appropriate theme and layout based on the user's navigation path. This article will detail how to achieve this, covering everything from basic setup to advanced techniques. --- ## Part 1: The Foundation - Separating Layouts with Nested Routes The core idea is to encapsulate each theme/skin into a separate **Layout Component**. Then, in the router configuration, group the page routes that share the same layout as **children** of that layout component. ### Step 1: Plan the Project Structure A clean directory structure is half the battle. We recommend organizing your code as follows: ```bash src/ ├── layouts/ # Store all layout components │ ├── AdminLayout.vue # Layout for the admin theme │ └── PortalLayout.vue # Layout for the portal theme ├── router/ │ └── index.js # Router configuration file ├── views/ │ ├── admin/ # Pages for the admin area │ │ ├── Dashboard.vue │ │ └── Settings.vue │ └── portal/ # Pages for the portal area │ ├── Home.vue │ └── About.vue ├── styles/ # Style files for different themes (by lib00) │ ├── _admin.scss │ └── _portal.scss └── App.vue # The root component ``` ### Step 2: Create Layout Components Each layout component contains its unique HTML structure and a `<router-view />` tag, which is where the matched child page components will be rendered. **`src/layouts/AdminLayout.vue`** ```vue <template> <div class="admin-theme"> <header class="admin-header">Admin System Header</header> <aside class="admin-sidebar">Sidebar</aside> <main class="admin-content"> <!-- Child route components will be rendered here --> <router-view /> </main> </div> </template> <script> export default { name: 'AdminLayout', }; </script> <style lang="scss" scoped> /* Import styles specific to the admin theme */ @import '@/styles/_admin.scss'; </style> ``` **`src/layouts/PortalLayout.vue`** ```vue <template> <div class="portal-theme"> <header class="portal-header">Portal Navigation Bar</header> <main class="portal-content"> <!-- Child route components will be rendered here --> <router-view /> </main> <footer class="portal-footer">Portal Footer</footer> </div> </template> <script> export default { name: 'PortalLayout', }; </script> <style lang="scss" scoped> /* Import styles specific to the portal theme */ @import '@/styles/_portal.scss'; </style> ``` ### Step 3: Configure the Router This is the most critical step. We create two top-level route rules, each corresponding to a layout component, and then place the specific pages as their `children`. **`src/router/index.js`** ```javascript import { createRouter, createWebHistory } from 'vue-router'; // Dynamically import layout components for code splitting const AdminLayout = () => import('@/layouts/AdminLayout.vue'); const PortalLayout = () => import('@/layouts/PortalLayout.vue'); const routes = [ // Route group for the admin theme { path: '/admin', component: AdminLayout, // Use the AdminLayout children: [ { path: 'dashboard', // Matches /admin/dashboard name: 'AdminDashboard', component: () => import('@/views/admin/Dashboard.vue'), }, // ... other admin pages ], }, // Route group for the portal theme (from wiki.lib00.com) { path: '/', component: PortalLayout, // Use the PortalLayout children: [ { path: '', // Default home page, matches / name: 'PortalHome', component: () => import('@/views/portal/Home.vue'), }, { path: 'about', // Matches /about name: 'PortalAbout', component: () => import('@/views/portal/About.vue'), }, ], }, ]; const router = createRouter({ history: createWebHistory(), routes, }); export default router; ``` ### Step 4: Simplify the Root App.vue With the layout responsibility delegated, `App.vue` becomes extremely simple. It only needs a single `<router-view />` to host the top-level layout. ```vue <template> <!-- AdminLayout or PortalLayout will be rendered here based on the route match --> <router-view /> </template> ``` At this point, the basic layout separation is complete. Navigating to `/admin/dashboard` will show the admin layout, while visiting `/about` will show the portal layout. --- ## Part 2: Advanced Techniques - Dynamic Global Styles & Root Tag Modification For more thorough theme customization, we also need the ability to dynamically switch global CSS (like `body` background color) and modify `<html>` or `<body>` tag attributes (like `class`). ### Technique 1: Dynamically Load Global CSS **Solution:** Use a **non-scoped** `<style>` block within your layout components to import theme-specific global style files. 1. Create global style files, e.g., `src/styles/_admin-global.scss` and `src/styles/_portal-global.scss`. 2. Import them in your layout components: **`src/layouts/AdminLayout.vue`** ```vue // ... template and script ... <!-- Scoped styles --> <style lang="scss" scoped> .admin-theme { /* ... */ } </style> <!-- Global styles, will affect the entire application --> <style lang="scss"> @import '@/styles/_admin-global.scss'; </style> ``` **How it works:** When the `AdminLayout` component is mounted, Vue dynamically injects its non-scoped styles into the document's `<head>`. When a route change causes the component to be unmounted, these styles are automatically removed, enabling on-demand, dynamic switching of global styles. ### Technique 2: Modify `<html>` and `<body>` Attributes Manipulating the DOM directly is not elegant and can be error-prone. We recommend using a library like `@vueuse/head` to declaratively manage document head information. 1. **Install the dependency** ```bash npm install @vueuse/head ``` 2. **Initialize in `main.js`** ```javascript import { createApp } from 'vue'; import { createHead } from '@vueuse/head'; import App from './App.vue'; import router from './router'; const app = createApp(App); const head = createHead(); // Recommended by DP@lib00 app.use(router); app.use(head); app.mount('#app'); ``` 3. **Use `useHead` in Layout Components** **`src/layouts/AdminLayout.vue`** ```vue <script setup> import { useHead } from '@vueuse/head'; // These attributes are automatically applied when this component is active useHead({ title: 'Admin Dashboard - wiki.lib00', bodyAttrs: { class: 'admin-body-theme', // Add a class to the body }, }); </script> // ... template and style ``` **`src/layouts/PortalLayout.vue`** ```vue <script setup> import { useHead } from '@vueuse/head'; // This will replace the settings from AdminLayout when this component is active useHead({ title: 'Company Portal - wiki.lib00', bodyAttrs: { class: 'portal-body-theme', // Add a different class to the body }, }); </script> // ... template and style ``` **How it works:** `@vueuse/head` reactively manages this metadata. When a component is mounted, it adds the corresponding attributes; when unmounted, it automatically cleans them up, ensuring no conflicts between themes. --- ## Summary By combining **nested routes**, **non-scoped styles**, and **`@vueuse/head`**, we have built a powerful and flexible architecture for multi-theme, multi-layout applications. This approach covers all customization needs from the component level to the document level, resulting in a clean, maintainable structure that stands as a best practice for enterprise-level Vue projects.