Progress bars are used in dashboards for everything from skill sections to file uploads to multi-step forms. Here's the reusable component pattern I use.
Basic Progress Bar Component
// components/progress-bar/progress-bar.component.ts import { Component, Input, OnInit, signal } from '@angular/core' import { CommonModule } from '@angular/common' export type ProgressColor = 'primary' | 'success' | 'danger' | 'warning' | 'info' @Component({ selector: 'app-progress-bar', standalone: true, imports: [CommonModule], template: ` <div> @if (label) { <div class="d-flex justify-content-between mb-1"> <span class="small fw-semibold">{{ label }}</span> <span class="small text-muted">{{ animatedValue() }}%</span> </div> } <div class="progress" [style.height]="height + 'px'"> <div class="progress-bar" role="progressbar" [class]="colorClass" [class.progress-bar-striped]="striped" [class.progress-bar-animated]="animated" [style.width]="animatedValue() + '%'" [attr.aria-valuenow]="animatedValue()" aria-valuemin="0" aria-valuemax="100"> </div> </div> </div> `, styles: [` .progress-bar { transition: width 0.8s ease; } `] }) export class ProgressBarComponent implements OnInit { @Input() value = 0 @Input() label = '' @Input() color: ProgressColor = 'primary' @Input() striped = false @Input() animated = false @Input() height = 8 animatedValue = signal(0) get colorClass(): string { const map: Record<ProgressColor, string> = { primary: 'bg-primary', success: 'bg-success', danger: 'bg-danger', warning: 'bg-warning', info: 'bg-info' } return map[this.color] } ngOnInit() { // Delay to trigger CSS transition on load setTimeout(() => this.animatedValue.set(this.value), 100) } }
Skills Section Component
// components/skills/skills.component.ts import { Component } from '@angular/core' import { ProgressBarComponent } from '../progress-bar/progress-bar.component' @Component({ selector: 'app-skills', standalone: true, imports: [ProgressBarComponent], template: ` <div class="card border-0 shadow-sm"> <div class="card-body p-4"> <h5 class="fw-bold mb-4">Technical Skills</h5> <div class="d-flex flex-column gap-3"> @for (skill of skills; track skill.name) { <app-progress-bar [value]="skill.level" [label]="skill.name" [color]="skill.color" [height]="10" /> } </div> </div> </div> ` }) export class SkillsComponent { skills = [ { name: 'Angular 21', level: 95, color: 'danger' as const }, { name: 'Bootstrap 5', level: 92, color: 'primary' as const }, { name: 'TypeScript', level: 88, color: 'info' as const }, { name: 'Node.js', level: 80, color: 'success' as const }, { name: 'UiPath RPA', level: 75, color: 'warning' as const }, ] }
Real-Time Progress (File Upload)
// components/upload-progress/upload-progress.component.ts import { Component, signal } from '@angular/core' import { CommonModule } from '@angular/common' @Component({ selector: 'app-upload-progress', standalone: true, imports: [CommonModule], template: ` <div class="card border-0 shadow-sm p-4" style="max-width:400px;"> <h6 class="fw-bold mb-3">File Upload</h6> @if (!isComplete()) { <div class="d-flex align-items-center gap-3 mb-3"> <div class="text-muted small flex-grow-1 text-truncate">{{ fileName }}</div> <span class="small fw-bold" style="color:#fd4766;">{{ progress() }}%</span> </div> <div class="progress mb-3" style="height:6px;"> <div class="progress-bar progress-bar-striped progress-bar-animated" style="background:#fd4766;transition:width 0.3s ease;" [style.width]="progress() + '%'"> </div> </div> <button class="btn btn-sm btn-outline-secondary" (click)="cancelUpload()">Cancel</button> } @if (isComplete()) { <div class="alert alert-success py-2 mb-0 d-flex align-items-center gap-2"> <span>✅</span> <span class="small">Upload complete!</span> </div> } @if (!isUploading() && !isComplete()) { <button class="btn btn-sm text-white" style="background:#fd4766;" (click)="startUpload()">Simulate Upload</button> } </div> ` }) export class UploadProgressComponent { fileName = 'dashboard-template.zip' progress = signal(0) isUploading = signal(false) isComplete = signal(false) private timer?: ReturnType<typeof setInterval> startUpload() { this.isUploading.set(true) this.isComplete.set(false) this.progress.set(0) this.timer = setInterval(() => { this.progress.update(v => { if (v >= 100) { clearInterval(this.timer) this.isComplete.set(true) this.isUploading.set(false) return 100 } return v + Math.floor(Math.random() * 8) + 2 }) }, 150) } cancelUpload() { clearInterval(this.timer) this.isUploading.set(false) this.progress.set(0) } }
The UploadProgressComponent simulates a real upload — the progress increments in random steps just like a real file upload would. Replace the setInterval with actual HttpClient upload events and it works for real uploads.
Frequently Asked Questions
Set the initial width to 0 and use Angular's transition with a delayed signal update. In ngOnInit: setTimeout(() => this.progress.set(this.targetValue), 100). The CSS transition on .progress-bar handles the animation: .progress-bar { transition: width 0.6s ease; }.
Use a signal for the progress value: progress = signal(0). Update it from your service or interval: setInterval(() => this.progress.update(v => Math.min(v + 10, 100)), 500). The template binds [style.width]='progress() + "%"' and updates automatically.
Add the text inside the progress-bar div: <div class='progress-bar'>{{ progress() }}%</div>. For accessibility add aria-valuenow, aria-valuemin and aria-valuemax attributes bound to the progress value.
Related
Need a Full Angular + Bootstrap Admin Dashboard?
Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.
Browse Templates →Use code FIRST30 for 30% off.