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 / APIPurpose
border-dangerRed border on highlighted plan
shadow-lg / shadow-smDepth differentiation between plans
position-absolute top-0 start-50 translate-middleCenter badge above card
form-check form-switchBootstrap toggle switch for billing period
signal()Reactive billing period state
@Input() isAnnualPass billing period down to card
flex-grow-1Push CTA button to bottom of card
table-lightAlternating 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.