DEV Community

Sakshi
Sakshi

Posted on

How to Set Up Angular 22 with Bootstrap 5: Step by Step

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
            ]
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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';
Enter fullscreen mode Exit fullscreen mode

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]))
  ]
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
]
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 }
  })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

@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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)