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

Use @Input() decorators for the card's data properties and ng-content for the card body. Create an interface for the card data type. This lets parent components pass data and customize content via content projection.
Add <ng-content> inside the card-body div. The parent component wraps content inside the selector tags: <app-card><p>Custom content</p></app-card>. Use named slots with select attribute for multiple content areas: <ng-content select='[card-header]'>.
Create a StatsCard component with @Input() for title, value, change, icon and color. Use computed() or a getter to determine if the change is positive or negative. Bind Bootstrap color utilities based on the change direction.
Use ngFor on a Bootstrap row with col-md-4 columns: <div class='col-md-4' *ngFor='let card of cards'><app-card [data]='card'></app-card></div>. Add h-100 to the card for equal heights.
Bootstrap cards if you're already using Bootstrap in your project — consistent styling system. Angular Material mat-card if you're building a Material Design app. Don't mix both in the same project unless you have a specific reason.

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.