Reusable card components are one of the first things I build in any Angular project. Here are the patterns I use — from a simple generic card to a stats card with trend indicators.
Generic Card Component
// card.component.ts import { Component, Input } from '@angular/core' import { CommonModule } from '@angular/common' @Component({ selector: 'app-card', standalone: true, imports: [CommonModule], template: ` <div class="card border-0 shadow-sm h-100" [ngClass]="cardClass"> <div class="card-header border-0 bg-transparent pb-0" *ngIf="title || headerSlot"> <div class="d-flex justify-content-between align-items-center"> <h6 class="fw-bold mb-0" *ngIf="title">{{ title }}</h6> <ng-content select="[card-header-action]"></ng-content> </div> </div> <div class="card-body" [class.pt-2]="title"> <ng-content></ng-content> </div> <div class="card-footer bg-transparent border-0" *ngIf="footer"> <ng-content select="[card-footer]"></ng-content> </div> </div> ` }) export class CardComponent { @Input() title = '' @Input() footer = false @Input() headerSlot = false @Input() cardClass = '' }
Use:
<!-- Simple card --> <app-card title="Recent Activity"> <p class="text-muted small">No recent activity to show.</p> </app-card> <!-- Card with header action --> <app-card title="Users" [headerSlot]="true"> <button class="btn btn-sm btn-primary" card-header-action>Add User</button> <p>Card content here</p> </app-card>
Stats Card Component
This is the one I use in every dashboard:
// stats-card.component.ts import { Component, Input, computed, signal } from '@angular/core' import { CommonModule } from '@angular/common' export interface StatsCardData { title: string value: string | number change?: number // percentage change, positive or negative icon?: string // emoji or icon class color?: 'red' | 'blue' | 'green' | 'yellow' } @Component({ selector: 'app-stats-card', standalone: true, imports: [CommonModule], template: ` <div class="card border-0 shadow-sm"> <div class="card-body p-3"> <div class="d-flex align-items-start justify-content-between"> <div> <p class="text-muted small mb-1">{{ data.title }}</p> <h3 class="fw-bold mb-0">{{ data.value }}</h3> </div> <div class="rounded p-2" [style.background]="iconBg"> <span style="font-size:1.2rem;">{{ data.icon || '📊' }}</span> </div> </div> <div class="mt-2" *ngIf="data.change !== undefined"> <span class="small fw-semibold" [class.text-success]="data.change >= 0" [class.text-danger]="data.change < 0"> {{ data.change >= 0 ? '↑' : '↓' }} {{ data.change | number:'1.1-1' }}% </span> <span class="text-muted small ms-1">vs last month</span> </div> </div> </div> ` }) export class StatsCardComponent { @Input() data!: StatsCardData get iconBg(): string { const colors: Record<string, string> = { red: 'rgba(253,71,102,0.1)', blue: 'rgba(79,110,247,0.1)', green: 'rgba(16,185,129,0.1)', yellow: 'rgba(245,158,11,0.1)', } return colors[this.data?.color || 'blue'] } }
Use in a dashboard:
<div class="row g-3"> <div class="col-sm-6 col-xl-3"> <app-stats-card [data]="{ title: 'Total Revenue', value: '$12,480', change: 8.2, icon: '💰', color: 'green' }"></app-stats-card> </div> <div class="col-sm-6 col-xl-3"> <app-stats-card [data]="{ title: 'Downloads', value: '3,240', change: -2.4, icon: '📥', color: 'blue' }"></app-stats-card> </div> <div class="col-sm-6 col-xl-3"> <app-stats-card [data]="{ title: 'Active Users', value: '1,847', change: 12.5, icon: '👥', color: 'red' }"></app-stats-card> </div> <div class="col-sm-6 col-xl-3"> <app-stats-card [data]="{ title: 'Templates', value: '48', icon: '🎨', color: 'yellow' }"></app-stats-card> </div> </div>
Card List with ngFor
For a list of cards from an API or data array:
// template-list.component.ts @Component({ standalone: true, imports: [CommonModule, CardComponent], template: ` <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"> <div class="col" *ngFor="let template of templates; trackBy: trackById"> <div class="card border-0 shadow-sm h-100"> <img [src]="template.image" class="card-img-top" [alt]="template.name"> <div class="card-body d-flex flex-column"> <span class="badge mb-2 align-self-start" [style.background]="frameworkColor(template.framework)"> {{ template.framework }} </span> <h6 class="card-title fw-bold mb-1">{{ template.name }}</h6> <p class="card-text text-muted small flex-grow-1">{{ template.description }}</p> <div class="d-flex justify-content-between align-items-center mt-2"> <span class="fw-bold" style="color:#fd4766;">\${{ template.price }}</span> <a [href]="template.url" class="btn btn-sm text-white" style="background:#fd4766;"> View </a> </div> </div> </div> </div> </div> ` }) export class TemplateListComponent { templates = [ { id: 1, name: 'Marvel Dashboard', framework: 'Angular', price: 29, description: 'Angular 21 admin template', image: '/assets/marvel.jpg', url: '#' }, { id: 2, name: 'PORTO Template', framework: 'Bootstrap', price: 19, description: 'Bootstrap 5 multipurpose', image: '/assets/porto.jpg', url: '#' }, ] trackById(index: number, item: any) { return item.id } frameworkColor(framework: string): string { const colors: Record<string, string> = { 'Angular': '#dd0031', 'Bootstrap': '#7952b3', 'React': '#61dafb', 'Next.js': '#000', } return colors[framework] || '#6b7280' } }
Always use trackBy on ngFor with cards — it prevents Angular from re-rendering all cards when the data updates. For large lists this makes a significant performance difference.
Frequently Asked Questions
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.