Comparison Table
blockmarketingPlan comparison feature matrix (Starter / Pro / Enterprise) with a sticky header row, monthly/yearly toggle that flips prices live, grouped feature rows (Core / Collaboration / Support), and check / minus / short-value cells. The Pro column is highlighted with a "Popular" badge and a primary tint down the whole column.
Installation
$pnpm dlx uipkge-ng@latest add comparison-table$npx uipkge-ng@latest add comparison-table$yarn dlx uipkge-ng@latest add comparison-table$bunx uipkge-ng@latest add comparison-tableManifest:
https://uipkge.dev/r/angular/comparison-table.jsonInstalls to:src/app/components/blocks/comparison-table/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.
ComparisonPlaninterface ComparisonPlan {
name: string
monthly: number
yearly: number
cta: string
popular?: boolean
}ComparisonFeatureinterface ComparisonFeature {
label: string
cells: [ComparisonCell, ComparisonCell, ComparisonCell]
}ComparisonGroupinterface ComparisonGroup {
label: string
features: ComparisonFeature[]
}npm dependencies
Includes
Files installed (2)
src/app/components/blocks/comparison-table/comparison-table.component.ts7.7 kB
// Plan comparison feature matrix (Starter / Pro / Enterprise) with a monthly/yearly // toggle that flips prices live, grouped feature rows, and check / minus / value cells. // The Pro column is highlighted with a "Popular" badge and a primary tint. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } 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 { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component' export type BillingCycle = 'monthly' | 'yearly' export type ComparisonCell = boolean | string | number export interface ComparisonPlan { name: string monthly: number yearly: number cta: string popular?: boolean } export interface ComparisonFeature { label: string cells: [ComparisonCell, ComparisonCell, ComparisonCell] } export interface ComparisonGroup { label: string features: ComparisonFeature[] } const defaultPlans: ComparisonPlan[] = [ { name: 'Starter', monthly: 9, yearly: 7, cta: 'Start free' }, { name: 'Pro', monthly: 29, yearly: 24, cta: 'Start 14-day trial', popular: true }, { name: 'Enterprise', monthly: 79, yearly: 65, cta: 'Talk to sales' }, ] const defaultGroups: ComparisonGroup[] = [ { label: 'Core', features: [ { label: 'Projects', cells: [3, 'Unlimited', 'Unlimited'] }, { label: 'Team seats', cells: ['5 seats', '20 seats', 'Custom'] }, { label: 'API access', cells: [false, true, true] }, ], }, { label: 'Collaboration', features: [ { label: 'Shared workspaces', cells: [false, true, true] }, { label: 'Guest reviewers', cells: [false, '5 guests', 'Unlimited'] }, { label: 'Roles & permissions', cells: [true, true, true] }, { label: 'Audit log', cells: [false, false, true] }, ], }, { label: 'Support', features: [ { label: 'Support channel', cells: ['Email', 'Priority email', 'Dedicated CSM'] }, { label: 'Guided onboarding', cells: [false, true, true] }, { label: 'Uptime SLA', cells: [false, false, '99.9%'] }, ], }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-comparison-table, [ui-comparison-table]', 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, UiToggleGroupComponent, UiToggleGroupItemComponent, ], template: ` <section data-slot="comparison-table" [class]="rootClass"> <div class="mx-auto max-w-5xl px-6 py-16"> <div class="mb-8 flex flex-col items-center gap-4 text-center"> <h2 class="text-2xl font-semibold tracking-tight sm:text-3xl">Compare plans</h2> <ui-toggle-group type="single" [value]="cycle()" (valueChange)="setCycle($event)"> <button ui-toggle-group-item value="monthly">Monthly</button> <button ui-toggle-group-item value="yearly"> Yearly <span ui-badge variant="secondary" class="ml-2">−20%</span> </button> </ui-toggle-group> </div> <div class="overflow-x-auto rounded-lg border"> <table class="w-full max-w-[640px] min-w-full border-separate border-spacing-0 text-sm"> <caption class="sr-only"> Feature comparison across Starter, Pro, and Enterprise plans </caption> <thead> <tr> <th scope="col" class="bg-background/95 supports-[backdrop-filter]:bg-background/80 text-muted-foreground sticky top-0 left-0 z-10 w-1/4 border-b p-4 text-left font-medium backdrop-blur" > <span class="sr-only">Feature</span> </th> @for (plan of plans; track plan.name) { <th scope="col" [class]=" plan.popular ? 'bg-primary/10 supports-[backdrop-filter]:bg-primary/10 sticky top-0 z-10 border-b p-4 text-center align-bottom backdrop-blur' : 'bg-background/95 supports-[backdrop-filter]:bg-background/80 sticky top-0 z-10 border-b p-4 text-center align-bottom backdrop-blur' " > <div class="flex flex-col items-center gap-1.5"> @if (plan.popular) { <span ui-badge>Popular</span> } <span class="text-base font-semibold tracking-tight">{{ plan.name }}</span> <span class="tabular-nums"> <span class="text-2xl font-semibold">\${{ priceFor(plan) }}</span> <span class="text-muted-foreground text-xs"> /mo</span> </span> <button ui-button size="sm" [variant]="plan.popular ? 'default' : 'outline'" class="mt-1 w-full"> {{ plan.cta }} </button> </div> </th> } </tr> </thead> @for (group of groups; track group.label) { <tbody> <tr> <th colspan="4" scope="colgroup" class="bg-muted/50 text-muted-foreground border-b p-3 text-left text-xs font-semibold tracking-widest uppercase" > {{ group.label }} </th> </tr> @for (feature of group.features; track feature.label) { <tr class="hover:bg-muted/30"> <th scope="row" class="border-b p-4 text-left align-middle font-normal"> {{ feature.label }} </th> @for (cell of feature.cells; track $index) { <td [class]=" 'border-b p-4 text-center align-middle' + (plans[$index].popular ? ' bg-primary/5' : '') " > @if (cell === true) { <lucide-icon [img]="Check" class="text-foreground mx-auto size-4" aria-label="Included" /> } @else if (cell === false) { <lucide-icon [img]="Minus" class="text-muted-foreground/50 mx-auto size-4" aria-label="Not included" /> } @else { <span class="text-xs font-medium">{{ cell }}</span> } </td> } </tr> } </tbody> } </table> </div> </div> </section> `, }) export class UiComparisonTableComponent { protected readonly Check = Check protected readonly Minus = Minus @Input() plans: ComparisonPlan[] = defaultPlans @Input() groups: ComparisonGroup[] = defaultGroups @Input() set initialCycle(v: BillingCycle | undefined) { if (v) this.cycle.set(v) } @Input('class') className?: string readonly cycle = signal<BillingCycle>('monthly') get rootClass(): string { return cn('bg-background w-full', this.className) } setCycle(v: unknown): void { if (v === 'monthly' || v === 'yearly') this.cycle.set(v) } priceFor(plan: ComparisonPlan): number { return this.cycle() === 'monthly' ? plan.monthly : plan.yearly } }src/app/components/blocks/comparison-table/index.ts0.2 kB
export { UiComparisonTableComponent, type BillingCycle, type ComparisonCell, type ComparisonPlan, type ComparisonFeature, type ComparisonGroup, } from './comparison-table.component'
Raw manifest:https://uipkge.dev/r/angular/comparison-table.json