Menu

Change language

GitHub repo

Pricing — Per-Plan Comparison

blockmarketing

Plan-first comparison that renders each plan as its own card of included and excluded capabilities, the layout a wide matrix should degrade to on a phone.

Installation

$npx uipkge-ng@latest add pricing-matrix-plan-cards
Manifest:https://uipkge.dev/r/angular/pricing-matrix-plan-cards.jsonInstalls to:src/app/components/blocks/pricing-matrix-plan-cards/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

PricingPlanCard
interface PricingPlanCard {
  name: string
  price: string
  cadence: string
  included: number[]
  note: string
  featured?: boolean
}

Used by

Files installed (2)

  • src/app/components/blocks/pricing-matrix-plan-cards/pricing-matrix-plan-cards.component.ts5.3 kB
    // Plan-first comparison that renders each plan as its own card of included and
    // excluded capabilities, the layout a wide matrix should degrade to on a phone.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { Check, LucideAngularModule, Minus } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export interface PricingPlanCard {
      name: string
      price: string
      cadence: string
      included: number[]
      note: string
      featured?: boolean
    }
    
    // One capability list, three plans referencing it by index — a capability can
    // never appear on one card and silently vanish from another.
    const capabilities = [
      'Certified metric definitions',
      'Connected warehouses',
      'Query budget per team',
      'Row-level access control',
      'SCIM provisioning',
      'Audit log export',
      'Embedded dashboards',
      'Data residency choice',
      'Customer-managed keys',
      'Named support engineer',
    ]
    
    const plans: PricingPlanCard[] = [
      { name: 'Team', price: '$0', cadence: 'while in beta', included: [0, 1], note: 'For one team proving it out.' },
      {
        name: 'Business',
        price: '$1,400',
        cadence: 'per month',
        included: [0, 1, 2, 3, 4, 5, 6],
        note: 'For finance and analytics running together.',
        featured: true,
      },
      {
        name: 'Enterprise',
        price: 'Custom',
        cadence: 'annual',
        included: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        note: 'For regulated estates and multi-region.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-pricing-matrix-plan-cards, [ui-pricing-matrix-plan-cards]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="pricing-matrix-plan-cards" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">Plans</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What each plan actually includes</h2>
              <p class="text-muted-foreground mt-3 text-lg">
                The same list on every card, with what is missing shown rather than omitted.
              </p>
            </div>
    
            <div class="mt-10 grid gap-4 lg:grid-cols-3">
              @for (plan of plans; track plan.name) {
                <div ui-card [class]="plan.featured ? 'border-primary' : ''">
                  <div ui-card-content class="flex h-full flex-col p-6">
                    <div class="flex items-center justify-between gap-3">
                      <p class="text-sm font-semibold">{{ plan.name }}</p>
                      @if (plan.featured) {
                        <span ui-badge variant="secondary">Most chosen</span>
                      }
                    </div>
    
                    <p class="font-display mt-4 text-3xl font-bold tracking-tight">{{ plan.price }}</p>
                    <p class="text-muted-foreground mt-1 text-xs">{{ plan.cadence }}</p>
                    <p class="text-muted-foreground mt-3 text-sm leading-relaxed">{{ plan.note }}</p>
    
                    <ui-separator class="my-5" />
    
                    <ul class="space-y-2.5">
                      @for (capability of capabilities; track capability) {
                        <li [class]="capabilityClass(plan, $index)">
                          @if (isIncluded(plan, $index)) {
                            <lucide-icon [img]="Check" class="text-success mt-0.5 size-4 shrink-0" aria-hidden="true" />
                          } @else {
                            <lucide-icon
                              [img]="Minus"
                              class="text-muted-foreground/40 mt-0.5 size-4 shrink-0"
                              aria-hidden="true"
                            />
                          }
                          <span>{{ capability }}</span>
                        </li>
                      }
                    </ul>
    
                    <button ui-button [variant]="plan.featured ? 'default' : 'outline'" class="mt-auto pt-0 [&]:mt-8">
                      {{ ctaLabel(plan) }}
                    </button>
                  </div>
                </div>
              }
            </div>
          </div>
        </section>
      `,
    })
    export class UiPricingMatrixPlanCardsComponent {
      protected readonly Check = Check
      protected readonly Minus = Minus
    
      readonly capabilities = capabilities
      readonly plans = plans
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      isIncluded(plan: PricingPlanCard, index: number): boolean {
        return plan.included.includes(index)
      }
    
      capabilityClass(plan: PricingPlanCard, index: number): string {
        return cn('flex items-start gap-2.5 text-sm', !this.isIncluded(plan, index) && 'text-muted-foreground/60')
      }
    
      ctaLabel(plan: PricingPlanCard): string {
        return plan.name === 'Enterprise' ? 'Talk to us' : `Start on ${plan.name}`
      }
    }
    
  • src/app/components/blocks/pricing-matrix-plan-cards/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/pricing-matrix-plan-cards.json