I set up Angular 22 with Bootstrap 5.3 more times than I can count — for client projects, for templates I build and sell, for side projects. The process is straightforward but there are specific decisions along the way that matter for long-term maintainability. This guide covers the setup and the decisions, not just the commands.
Create the Angular 22 Project
ng new my-dashboard --standalone --routing --style=scss
cd my-dashboard
Two flags matter here. --standalone creates the project with standalone components as the default — no AppModule, no NgModules. This is the Angular 22 recommended approach and you want it from the start. --style=scss gives you SCSS instead of CSS — required for Bootstrap's SCSS variable system.
When prompted: enable strict mode. Always. The compiler catches null reference errors, type mismatches, and undefined property access at build time instead of runtime. The initial configuration friction is worth it.
Install Bootstrap 5.3
npm install bootstrap@5.3
Do not install @ng-bootstrap/ng-bootstrap yet — only install it if you need Angular-native Bootstrap component behaviors like modals, dropdowns, and datepickers with Angular integration. For styling only — Bootstrap CSS is sufficient.
Configure Bootstrap in angular.json
{
"projects": {
"my-dashboard": {
"architect": {
"build": {
"options": {
"styles": [
"src/styles.scss"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}
}
}
}
}
Do not reference Bootstrap CSS directly in angular.json. Import it through SCSS instead — this gives you access to Bootstrap's SCSS variables and mixins.
Set Up SCSS Architecture
This is the step most tutorials skip and it's the most important one for long-term maintainability.
src/
styles/
_variables.scss — Bootstrap overrides + custom variables
_mixins.scss — custom mixins
_components.scss — custom component styles
_utilities.scss — custom utility classes
styles.scss — imports everything in correct order
// src/styles/_variables.scss
// Override Bootstrap variables BEFORE importing Bootstrap
// Brand colors
$primary: #fd4766;
$secondary: #6c757d;
$success: #28a745;
$info: #17a2b8;
$warning: #ffc107;
$danger: #dc3545;
$light: #f8f9fa;
$dark: #1a1f2e;
// Typography
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
$font-size-base: 0.9375rem;
$line-height-base: 1.6;
$font-weight-base: 400;
// Layout
$border-radius: 8px;
$border-radius-sm: 6px;
$border-radius-lg: 12px;
$border-radius-xl: 16px;
// Shadows
$box-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
$box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
$box-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.12);
// Spacing
$spacer: 1rem;
// Enable all Bootstrap utilities
$enable-negative-margins: true;
$enable-cssgrid: true;
// src/styles.scss — correct import order
// 1. Google Fonts (before everything)
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
// 2. Your Bootstrap variable overrides
@import 'styles/variables';
// 3. Bootstrap SCSS — uses your overrides
@import 'bootstrap/scss/bootstrap';
// 4. Your custom component styles — built on top of Bootstrap
@import 'styles/components';
@import 'styles/utilities';
Import order matters. Variables before Bootstrap. Bootstrap before your custom styles. Getting this wrong means your variable overrides don't apply.
Configure the App
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core'
import { provideRouter, withComponentInputBinding } from '@angular/router'
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { routes } from './app.routes'
import { authInterceptor } from './core/interceptors/auth.interceptor'
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor]))
]
}
withComponentInputBinding() enables binding route params directly to component inputs — a cleaner pattern than injecting ActivatedRoute everywhere. withInterceptors adds functional interceptors — the Angular 22 recommended approach over class-based interceptors.
Set Up Routing With Lazy Loading
// src/app/app.routes.ts
import { Routes } from '@angular/router'
import { authGuard } from './core/guards/auth.guard'
export const routes: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'auth',
loadComponent: () =>
import('./features/auth/login/login.component')
.then(m => m.LoginComponent)
},
{
path: 'dashboard',
loadComponent: () =>
import('./features/dashboard/dashboard.component')
.then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{
path: 'users',
loadComponent: () =>
import('./features/users/users.component')
.then(m => m.UsersComponent),
canActivate: [authGuard]
},
{
path: '**',
loadComponent: () =>
import('./features/not-found/not-found.component')
.then(m => m.NotFoundComponent)
}
]
Every route lazy loads its component. Initial bundle contains only the bootstrap code and the first rendered route. Users, settings, and other sections load on navigation.
Add the Auth Interceptor
// src/app/core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { inject } from '@angular/core'
import { AuthService } from '../services/auth.service'
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService)
const token = authService.getToken()
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
})
return next(authReq)
}
return next(req)
}
Functional interceptor — no class, no interface implementation, no constructor injection. inject() works inside functional interceptors because they run in an injection context. Cleaner and less boilerplate than class-based interceptors.
Add the Auth Guard
// src/app/core/guards/auth.guard.ts
import { inject } from '@angular/core'
import { CanActivateFn, Router } from '@angular/router'
import { AuthService } from '../services/auth.service'
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService)
const router = inject(Router)
if (authService.isAuthenticated()) {
return true
}
return router.createUrlTree(['/auth'], {
queryParams: { returnUrl: state.url }
})
}
Build Your First Dashboard Component
// src/app/features/dashboard/dashboard.component.ts
import { Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DashboardService } from './dashboard.service'
import { StatCardComponent } from '../../shared/components/stat-card/stat-card.component'
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, StatCardComponent],
template: `
<div class="container-fluid p-4">
<div class="row mb-4">
<div class="col">
<h1 class="h4 mb-0">Dashboard</h1>
<p class="text-muted small mb-0">Welcome back</p>
</div>
</div>
<div class="row g-4 mb-4">
@for (stat of stats(); track stat.id) {
<div class="col-12 col-sm-6 col-xxl-3">
<app-stat-card
[label]="stat.label"
[value]="stat.value"
[change]="stat.change"
[icon]="stat.icon"
/>
</div>
}
</div>
</div>
`
})
export class DashboardComponent {
private dashboardService = inject(DashboardService)
stats = this.dashboardService.stats
}
@for with track — Angular 22 control flow syntax. Standalone. Signals from service. No NgModule. This is the Angular 22 pattern throughout.
Dark Mode With Bootstrap 5.3
// src/app/core/services/theme.service.ts
import { Injectable, signal } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class ThemeService {
private _theme = signal<'light' | 'dark'>(
(localStorage.getItem('theme') as 'light' | 'dark') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
)
theme = this._theme.asReadonly()
constructor() {
this.applyTheme(this._theme())
}
toggleTheme() {
const newTheme = this._theme() === 'light' ? 'dark' : 'light'
this._theme.set(newTheme)
this.applyTheme(newTheme)
localStorage.setItem('theme', newTheme)
}
private applyTheme(theme: 'light' | 'dark') {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
Signal for theme state. data-bs-theme on the root element. Bootstrap 5.3 handles all component switching automatically. Custom components use CSS custom properties to switch alongside Bootstrap.
The Complete Folder Structure
src/
app/
core/
guards/
auth.guard.ts
interceptors/
auth.interceptor.ts
services/
auth.service.ts
theme.service.ts
features/
auth/
login/
login.component.ts
login.component.html
dashboard/
dashboard.component.ts
dashboard.service.ts
users/
users.component.ts
users.service.ts
shared/
components/
stat-card/
stat-card.component.ts
sidebar/
sidebar.component.ts
navbar/
navbar.component.ts
app.config.ts
app.routes.ts
app.component.ts
styles/
_variables.scss
_components.scss
_utilities.scss
styles.scss
Core for framework infrastructure. Features for application sections. Shared for reusable components. This structure scales — add a new feature section without touching anything outside its folder.
Browse Angular 22 + Bootstrap 5.3 admin dashboard templates at LettStart Design — built with this exact structure. Use FIRST30 for 30% off.
Top comments (0)