Pricing — Per-Plan Comparison
blockmarketingPlan-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
$pnpm dlx uipkge-ng@latest add pricing-matrix-plan-cards$npx uipkge-ng@latest add pricing-matrix-plan-cards$yarn dlx uipkge-ng@latest add pricing-matrix-plan-cards$bunx uipkge-ng@latest add pricing-matrix-plan-cardsManifest:
https://uipkge.dev/r/angular/pricing-matrix-plan-cards.jsonInstalls to:src/app/components/blocks/pricing-matrix-plan-cards/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
PricingPlanCardinterface PricingPlanCard {
name: string
price: string
cadence: string
included: number[]
note: string
featured?: boolean
}npm dependencies
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
export { UiPricingMatrixPlanCardsComponent, type PricingPlanCard } from './pricing-matrix-plan-cards.component'
Raw manifest:https://uipkge.dev/r/angular/pricing-matrix-plan-cards.json