Pricing tables are one of the most common landing page components. Here's how to build them properly in Angular 22 with Bootstrap 5 — starting with a generic reusable card and building up to a full billing toggle using signals.
Pricing Card Component
// pricing-card.component.ts import { Component, Input } from '@angular/core' import { NgFor, NgIf, NgClass } from '@angular/common' export interface PricingPlan { name: string price: number annualPrice?: number description: string features: string[] cta: string isHighlighted?: boolean badge?: string } @Component({ selector: 'app-pricing-card', standalone: true, imports: [NgFor, NgIf, NgClass], template: ` <div class="position-relative h-100"> <!-- Badge --> <span *ngIf="plan.badge" class="badge text-white position-absolute top-0 start-50 translate-middle px-3 py-2" style="background:#fd4766;z-index:1;font-size:0.75rem;"> {{ plan.badge }} </span> <div class="card h-100 border-2" [ngClass]="{ 'border-danger shadow-lg': plan.isHighlighted, 'border-light shadow-sm': !plan.isHighlighted }" > <div class="card-body p-4 d-flex flex-column"> <!-- Header --> <div class="mb-4"> <h5 class="fw-bold mb-1">{{ plan.name }}</h5> <p class="text-muted small mb-3">{{ plan.description }}</p> <div class="d-flex align-items-end gap-1"> <span class="display-5 fw-bold" [style.color]="plan.isHighlighted ? '#fd4766' : 'inherit'"> ${{ displayPrice }} </span> <span class="text-muted mb-2">/mo</span> </div> <p *ngIf="isAnnual && plan.annualPrice" class="text-success small mb-0"> Save ${{ (plan.price - plan.annualPrice) * 12 }}/year </p> </div> <!-- Features --> <ul class="list-unstyled flex-grow-1 mb-4"> <li *ngFor="let feature of plan.features" class="d-flex align-items-center gap-2 mb-2 small"> <span class="text-success fw-bold">✓</span> {{ feature }} </li> </ul> <!-- CTA --> <button class="btn w-100 fw-semibold" [ngClass]="plan.isHighlighted ? 'text-white' : 'btn-outline-secondary'" [style.background]="plan.isHighlighted ? '#fd4766' : ''" [style.border-color]="plan.isHighlighted ? '#fd4766' : ''" > {{ plan.cta }} </button> </div> </div> </div> ` }) export class PricingCardComponent { @Input() plan!: PricingPlan @Input() isAnnual = false get displayPrice(): number { return this.isAnnual && this.plan.annualPrice ? this.plan.annualPrice : this.plan.price } }
Basic Pricing Table
// pricing-table.component.ts import { Component, signal, computed } from '@angular/core' import { NgFor } from '@angular/common' import { PricingCardComponent, PricingPlan } from './pricing-card.component' @Component({ selector: 'app-pricing-table', standalone: true, imports: [NgFor, PricingCardComponent], template: ` <section class="py-5"> <div class="container"> <div class="text-center mb-5"> <h2 class="fw-bold">Simple, Transparent Pricing</h2> <p class="text-muted">No hidden fees. Cancel anytime.</p> <!-- Billing toggle --> <div class="d-flex align-items-center justify-content-center gap-3 mt-4"> <span class="small fw-semibold" [class.text-muted]="isAnnual()">Monthly</span> <div class="form-check form-switch mb-0"> <input class="form-check-input" type="checkbox" role="switch" [checked]="isAnnual()" (change)="isAnnual.update(v => !v)" style="width:2.5rem;height:1.25rem;cursor:pointer;" > </div> <span class="small fw-semibold" [class.text-muted]="!isAnnual()"> Annual <span class="badge ms-1 text-white" style="background:#10b981;font-size:0.7rem;">Save 20%</span> </span> </div> </div> <div class="row g-4 justify-content-center"> <div class="col-md-4" *ngFor="let plan of plans"> <app-pricing-card [plan]="plan" [isAnnual]="isAnnual()" /> </div> </div> </div> </section> ` }) export class PricingTableComponent { isAnnual = signal(false) plans: PricingPlan[] = [ { name: 'Starter', price: 9, annualPrice: 7, description: 'Perfect for side projects', features: ['5 components', '1 project', 'Community support', 'Basic analytics'], cta: 'Get Started', isHighlighted: false, }, { name: 'Pro', price: 29, annualPrice: 23, description: 'For professional developers', badge: 'Most Popular', features: ['Unlimited components', '10 projects', 'Priority support', 'Advanced analytics', 'Custom domain'], cta: 'Start Free Trial', isHighlighted: true, }, { name: 'Team', price: 79, annualPrice: 63, description: 'For growing teams', features: ['Everything in Pro', 'Unlimited projects', 'Team collaboration', 'SSO', 'SLA guarantee'], cta: 'Contact Sales', isHighlighted: false, }, ] }
Feature Comparison Table
For more detailed plan comparisons, a table layout works better than cards:
// pricing-comparison.component.ts import { Component } from '@angular/core' import { NgFor, NgIf } from '@angular/common' interface Feature { name: string starter: boolean | string pro: boolean | string team: boolean | string } @Component({ selector: 'app-pricing-comparison', standalone: true, imports: [NgFor, NgIf], template: ` <div class="table-responsive"> <table class="table align-middle"> <thead> <tr> <th class="border-0 w-40">Feature</th> <th class="text-center border-0"> <div class="fw-bold">Starter</div> <div class="text-muted small fw-normal">$9/mo</div> </th> <th class="text-center border-0"> <div class="fw-bold" style="color:#fd4766;">Pro</div> <div class="small fw-normal" style="color:#fd4766;">$29/mo</div> <span class="badge text-white" style="background:#fd4766;font-size:0.65rem;">Popular</span> </th> <th class="text-center border-0"> <div class="fw-bold">Team</div> <div class="text-muted small fw-normal">$79/mo</div> </th> </tr> </thead> <tbody> <tr *ngFor="let feature of features; let i = index" [class.table-light]="i % 2 === 0"> <td class="small fw-medium">{{ feature.name }}</td> <td class="text-center"><ng-container *ngTemplateOutlet="cell; context: { val: feature.starter }"></ng-container></td> <td class="text-center"><ng-container *ngTemplateOutlet="cell; context: { val: feature.pro }"></ng-container></td> <td class="text-center"><ng-container *ngTemplateOutlet="cell; context: { val: feature.team }"></ng-container></td> </tr> </tbody> </table> </div> <ng-template #cell let-val="val"> <span *ngIf="val === true" class="text-success fw-bold">✓</span> <span *ngIf="val === false" class="text-muted">—</span> <span *ngIf="typeof(val) === 'string'" class="small fw-medium">{{ val }}</span> </ng-template> ` }) export class PricingComparisonComponent { features: Feature[] = [ { name: 'Projects', starter: '3', pro: '10', team: 'Unlimited' }, { name: 'Components', starter: '50', pro: 'Unlimited', team: 'Unlimited' }, { name: 'Analytics', starter: false, pro: true, team: true }, { name: 'Priority support', starter: false, pro: true, team: true }, { name: 'Custom domain', starter: false, pro: true, team: true }, { name: 'SSO', starter: false, pro: false, team: true }, { name: 'SLA guarantee', starter: false, pro: false, team: true }, ] typeof = typeof }
Key Classes Reference
| Class / API | Purpose |
|---|---|
border-danger | Red border on highlighted plan |
shadow-lg / shadow-sm | Depth differentiation between plans |
position-absolute top-0 start-50 translate-middle | Center badge above card |
form-check form-switch | Bootstrap toggle switch for billing period |
signal() | Reactive billing period state |
@Input() isAnnual | Pass billing period down to card |
flex-grow-1 | Push CTA button to bottom of card |
table-light | Alternating row background |
Frequently Asked Questions
Create a PricingCardComponent with @Input() for name, price, features, and highlighted state. Use Bootstrap's card, badge, and btn classes for styling. Add a ring or border-primary class on the highlighted plan. Loop over a plans array with @for in the parent component.
Use a signal for the billing period: billing = signal<'monthly' | 'annual'>('monthly'). Use computed() to derive each plan's displayed price based on the signal value. Bind a Bootstrap form-check-input to toggle the signal with a click handler.
Pass an isHighlighted @Input() boolean to the pricing card component. Conditionally apply Bootstrap border and shadow utilities: [class.border-danger]="isHighlighted" and [class.shadow-lg]="isHighlighted". Add a positioned Badge above the card using position-relative and a negative top offset.
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.