Detailed documentation for developing with the Metis Bootstrap 5 Admin Template.
- Getting Started
- Project Structure
- Architecture
- Adding New Features
- Component Patterns
- Sidebar & Responsive Layout
- Styling Guide
- Build Configuration
- Node.js 18+
- npm or yarn
npm install
npm run dev # Start dev server at http://localhost:3000
npm run build # Production build to dist-modern/
npm run preview # Preview production build
npm run lint # Run ESLint
npm run format # Format with Prettiersrc-modern/
├── *.html # Page templates (each requires data-page attribute)
├── scripts/
│ ├── main.js # Application entry point
│ ├── components/ # Page-specific components
│ │ ├── analytics.js
│ │ ├── calendar.js
│ │ ├── dashboard.js
│ │ ├── files.js
│ │ ├── forms.js
│ │ ├── help.js
│ │ ├── messages.js
│ │ ├── orders.js
│ │ ├── products.js
│ │ ├── reports.js
│ │ ├── security.js
│ │ ├── settings.js
│ │ ├── sidebar.js # SidebarManager (desktop collapse + mobile overlay)
│ │ └── users.js
│ └── utils/
│ ├── theme-manager.js # Dark/light mode handling
│ ├── notifications.js # SweetAlert2 wrapper
│ └── icon-manager.js # Icon library abstraction
├── styles/
│ └── scss/
│ ├── abstracts/ # Variables, mixins, functions
│ ├── components/ # UI component styles
│ ├── layout/ # Header, sidebar, footer
│ ├── pages/ # Page-specific styles
│ ├── themes/ # Theme variants
│ └── main.scss # Main entry point
└── assets/ # Static assets (images, icons)
The AdminApp class in main.js orchestrates initialization:
- Core Managers - ThemeManager, NotificationManager, IconManager
- Bootstrap Components - Dropdowns, modals, tooltips, popovers
- Page Detection - Routes to correct component via
data-pageattribute - Alpine.js - Registers global data components and starts Alpine
Each HTML page must have data-page="pagename" on the <body> tag:
<body data-page="users" class="admin-layout">This triggers the corresponding component loader in main.js:
// main.js initPageComponents()
switch (currentPage) {
case 'users':
await import('./components/users.js');
break;
// ...
}The SidebarManager class in scripts/components/sidebar.js is the single source of truth for all sidebar toggle behavior. It is initialized by main.js on every page and handles two distinct modes:
- Desktop (>=992px): Toggles between full-width (280px) and collapsed (70px) sidebar via the
sidebar-collapsedclass on#admin-wrapper. State is persisted inlocalStorage. - Mobile (<992px): Opens the sidebar as a slide-in overlay with a backdrop. Supports closing via backdrop click, Escape key, and scroll-lock on the body.
Key elements:
| Selector | Role |
|---|---|
#admin-wrapper |
Receives sidebar-collapsed class on desktop |
#admin-sidebar |
The sidebar element; receives show class on mobile |
[data-sidebar-toggle] |
The hamburger button that triggers toggle() |
.sidebar-backdrop |
Semi-transparent overlay behind mobile sidebar |
Important: Do not add inline <script> blocks that also listen for [data-sidebar-toggle] clicks. The SidebarManager is the only handler needed. Duplicate listeners will cancel each other out on desktop (both toggle the same class on the same click).
Page components are loaded asynchronously for code splitting:
async initUsersPage() {
try {
await import('./components/users.js');
console.log('👥 Users page script loaded successfully');
} catch (error) {
console.error('Failed to load users page script:', error);
}
}Defined in main.js and available on all pages:
| Component | Purpose |
|---|---|
searchComponent |
Global search with results dropdown |
statsCounter |
Auto-incrementing stat displays |
themeSwitch |
Theme toggle state management |
iconDemo |
Icon provider switching demo |
-
Create HTML file -
src-modern/newpage.html<body data-page="newpage" class="admin-layout">
-
Add Vite entry point -
vite.config.jsrollupOptions: { input: { // ... existing entries newpage: resolve(..., 'src-modern/newpage.html'), } }
-
Create page styles -
src-modern/styles/scss/pages/_newpage.scss// Page-specific styles .newpage-container { // ... }
-
Import in main.scss
@import 'pages/newpage';
-
Create component -
src-modern/scripts/components/newpage.jsimport Alpine from 'alpinejs'; document.addEventListener('alpine:init', () => { Alpine.data('newpageComponent', () => ({ // state items: [], // lifecycle init() { console.log('Newpage component initialized'); }, // methods loadItems() { // ... } })); });
-
Register in main.js
async initNewpagePage() { try { await import('./components/newpage.js'); console.log('📄 Newpage script loaded successfully'); } catch (error) { console.error('Failed to load newpage script:', error); } } // Add to switch statement in initPageComponents() case 'newpage': await this.initNewpagePage(); break;
For components shared across pages, create in utils/ and import where needed:
// src-modern/scripts/utils/data-table.js
export class DataTable {
constructor(element, options = {}) {
this.element = element;
this.options = { ...this.defaults, ...options };
}
defaults = {
perPage: 10,
sortable: true
};
render() {
// ...
}
}Alpine.data('componentName', () => ({
// Reactive state
isLoading: false,
items: [],
selectedItem: null,
// Computed-like getters
get filteredItems() {
return this.items.filter(item => item.active);
},
// Lifecycle hook
init() {
this.loadItems();
},
// Methods
async loadItems() {
this.isLoading = true;
try {
// Simulated API call
await new Promise(resolve => setTimeout(resolve, 500));
this.items = [/* data */];
} finally {
this.isLoading = false;
}
},
selectItem(item) {
this.selectedItem = item;
}
}));<div x-data="componentName">
<template x-if="isLoading">
<div class="spinner-border"></div>
</template>
<template x-for="item in filteredItems" :key="item.id">
<div @click="selectItem(item)" x-text="item.name"></div>
</template>
</div>// Using NotificationManager (available globally)
window.AdminApp.notificationManager.success('Item saved!');
window.AdminApp.notificationManager.error('Something went wrong');
window.AdminApp.notificationManager.warning('Please review your input');
window.AdminApp.notificationManager.info('Tip: You can drag items to reorder');
// Or use SweetAlert2 directly
Swal.fire({
title: 'Confirm Delete',
text: 'This action cannot be undone',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Delete',
confirmButtonColor: '#dc3545'
}).then((result) => {
if (result.isConfirmed) {
// Delete logic
}
});const chartOptions = {
chart: {
type: 'area',
height: 350,
toolbar: { show: false }
},
series: [{
name: 'Revenue',
data: [31, 40, 28, 51, 42, 109, 100]
}],
xaxis: {
categories: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
colors: ['#6366f1']
};
const chart = new ApexCharts(document.querySelector('#chart'), chartOptions);
chart.render();The template uses a consistent lg breakpoint (992px) across all responsive behavior. Below 992px the layout switches to a mobile-optimized mode.
| Breakpoint | Sidebar | Header | Cards & Buttons |
|---|---|---|---|
| >= 992px (desktop) | Fixed left panel, collapsible to 70px mini sidebar | Full navbar with search bar | Standard sizing |
| < 992px (mobile) | Hidden off-screen, slides in as overlay | Compact navbar, hamburger in flow | Compact sizing with smaller padding |
On desktop, clicking the hamburger toggles the sidebar-collapsed class on #admin-wrapper:
- Expanded (default): Sidebar is
var(--sidebar-width)(280px) with icon + text labels - Collapsed: Sidebar shrinks to
var(--sidebar-mini-width)(70px) with icons only
The main content area shifts automatically via CSS:
// layout/_main.scss
.admin-main {
margin-left: var(--sidebar-width);
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-collapsed .admin-main {
margin-left: var(--sidebar-mini-width);
}On mobile, the sidebar is positioned off-screen with transform: translateX(-100%) and slides in when the show class is added:
// layout/_sidebar.scss
@media (max-width: 991.98px) {
.admin-sidebar {
transform: translateX(-100%);
z-index: 1041; // above backdrop, below modals
&.show {
transform: translateX(0);
box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
}
}
}The .sidebar-backdrop element provides a semi-transparent overlay behind the sidebar. It is created automatically by SidebarManager if not present in the HTML.
Mobile sidebar behaviors:
- Background scroll is locked (
overflow: hiddenon body) when sidebar is open - Backdrop click closes the sidebar
- Escape key closes the sidebar
- Resizing from mobile to desktop cleans up overlay state and restores collapsed preference
The hamburger button ([data-sidebar-toggle]) lives inside the header navbar, immediately after the .navbar-brand. On desktop, it is absolutely positioned at the right edge of the sidebar:
// components/_hamburger.scss
@media (min-width: 992px) {
.admin-header .hamburger-menu {
position: absolute;
left: calc(var(--sidebar-width) - 40px - 0.5rem);
top: 50%;
transform: translateY(-50%);
}
}On mobile, the hamburger sits in normal document flow within the navbar.
The template uses a deliberate z-index stack to avoid overlap conflicts:
| Layer | Z-Index | Element |
|---|---|---|
| Header | 1030 | .admin-header (Bootstrap $zindex-fixed) |
| Sidebar (desktop) | 1035 | .admin-sidebar |
| Backdrop | 1040 | .sidebar-backdrop ($zindex-modal-backdrop) |
| Sidebar (mobile) | 1041 | .admin-sidebar.show |
| Modals | 1050+ | Bootstrap modals |
Cards and buttons use compact sizing on mobile for better touch usability:
@media (max-width: 991.98px) {
.card {
// Reduced padding and margins
}
.btn {
// Smaller padding for touch targets
}
}These responsive adjustments are defined in components/_cards.scss and components/_buttons.scss.
Located in src-modern/styles/scss/abstracts/_variables.scss:
// Brand Colors
$primary: #6366f1;
$secondary: #64748b;
$success: #10b981;
$warning: #f59e0b;
$danger: #ef4444;
$info: #3b82f6;
// Typography
$font-family-sans-serif: "Inter", system-ui, sans-serif;
$font-size-base: 0.9rem;
// Spacing & Layout
$border-radius: 0.75rem;
$box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);Theme switching uses Bootstrap's data-bs-theme attribute:
// ThemeManager toggles this
document.documentElement.setAttribute('data-bs-theme', 'dark');Custom theme styles in src-modern/styles/scss/themes/:
[data-bs-theme="dark"] {
--custom-bg: #1e1e2d;
--custom-text: #a1a5b7;
}// _component-name.scss
.component-name {
// Base styles
padding: 1rem;
border-radius: $border-radius;
// Element
&__header {
font-weight: 600;
}
&__body {
padding: 1rem 0;
}
// Modifier
&--compact {
padding: 0.5rem;
}
// State
&.is-active {
border-color: $primary;
}
}Key settings:
export default defineConfig({
root: 'src-modern',
build: {
outDir: '../dist-modern',
rollupOptions: {
input: {
// Multi-page entries
}
}
},
server: {
port: 3000,
open: true
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler'
}
}
},
resolve: {
alias: {
'@': resolve(..., 'src-modern'),
'~bootstrap': resolve(..., 'node_modules/bootstrap')
}
}
});Use in imports:
import { something } from '@/scripts/utils/something.js';@import '~bootstrap/scss/functions';- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
IE11 is not supported.