Pricing — Grouped Matrix
blockmarketingFeature comparison matrix with collapsible capability groups, so a long plan comparison opens on the summary rows and expands only where the reader is looking.
Installation
$pnpm dlx uipkge-ng@latest add pricing-matrix-grouped$npx uipkge-ng@latest add pricing-matrix-grouped$yarn dlx uipkge-ng@latest add pricing-matrix-grouped$bunx uipkge-ng@latest add pricing-matrix-groupedManifest:
https://uipkge.dev/r/angular/pricing-matrix-grouped.jsonInstalls to:src/app/components/blocks/pricing-matrix-grouped/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.
PricingGroupedRowinterface PricingGroupedRow {
feature: string
values: (boolean | string)[]
}PricingGroupedGroupinterface PricingGroupedGroup {
id: string
label: string
summary: string
rows: PricingGroupedRow[]
}Used by
Files installed (2)
src/app/components/blocks/pricing-matrix-grouped/pricing-matrix-grouped.component.ts7.3 kB
// Feature comparison matrix with collapsible capability groups, so a long plan // comparison opens on the summary rows and expands only where the reader is looking. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core' import { Check, ChevronDown, 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' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface PricingGroupedRow { feature: string values: (boolean | string)[] } export interface PricingGroupedGroup { id: string label: string summary: string rows: PricingGroupedRow[] } const plans = ['Team', 'Business', 'Enterprise'] const groups: PricingGroupedGroup[] = [ { id: 'modelling', label: 'Modelling', summary: 'Definitions, versioning, and review', rows: [ { feature: 'Certified definitions', values: ['50', '500', 'Unlimited'] }, { feature: 'Version history', values: ['30 days', 'Unlimited', 'Unlimited'] }, { feature: 'Required review', values: [false, true, true] }, { feature: 'Period locking', values: [false, true, true] }, ], }, { id: 'access', label: 'Access & identity', summary: 'Who sees which rows', rows: [ { feature: 'SSO (SAML)', values: [true, true, true] }, { feature: 'SCIM provisioning', values: [false, true, true] }, { feature: 'Row-level scope', values: [false, true, true] }, { feature: 'Customer-managed keys', values: [false, false, true] }, ], }, { id: 'ops', label: 'Operations', summary: 'Cost, alerting, and support', rows: [ { feature: 'Query budgets', values: [false, true, true] }, { feature: 'Drift alerting', values: [false, true, true] }, { feature: 'Audit log export', values: [false, '90 days', 'Unlimited'] }, { feature: 'Named support engineer', values: [false, false, true] }, ], }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-pricing-matrix-grouped, [ui-pricing-matrix-grouped]', 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, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableRowComponent, ], template: ` <section data-slot="pricing-matrix-grouped" [class]="rootClass"> <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28"> <span ui-badge variant="secondary">Compare plans</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Grouped, so it stays readable</h2> <div ui-card class="mt-8"> <div ui-card-content class="p-0"> <div class="text-muted-foreground grid grid-cols-[1fr_repeat(3,5rem)] gap-2 px-4 py-3 text-xs font-medium sm:grid-cols-[1fr_repeat(3,7rem)]" > <span>Capability</span> @for (plan of plans; track plan) { <span class="text-center">{{ plan }}</span> } </div> <ui-separator /> @for (group of groups; track group.id) { <div> <button type="button" class="hover:bg-muted focus-visible:ring-ring flex w-full items-center gap-3 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none" [attr.aria-expanded]="isOpen(group.id)" [attr.aria-controls]="'group-' + group.id" (click)="toggle(group.id)" > <lucide-icon [img]="ChevronDown" [class]="chevronClass(group.id)" aria-hidden="true" /> <span class="min-w-0"> <span class="block text-sm font-medium">{{ group.label }}</span> <span class="text-muted-foreground block text-xs">{{ group.summary }}</span> </span> <span class="text-muted-foreground ml-auto shrink-0 font-mono text-xs"> {{ group.rows.length }} </span> </button> @if (isOpen(group.id)) { <div [id]="'group-' + group.id"> <table ui-table> <tbody ui-table-body> @for (row of group.rows; track row.feature) { <tr ui-table-row> <td ui-table-cell class="pl-11 text-sm">{{ row.feature }}</td> @for (value of row.values; track $index) { <td ui-table-cell class="w-20 text-center sm:w-28"> @if (value === true) { <lucide-icon [img]="Check" class="text-success mx-auto size-4" aria-hidden="true" /> <span class="sr-only">Included</span> } @else if (value === false) { <lucide-icon [img]="Minus" class="text-muted-foreground/50 mx-auto size-4" aria-hidden="true" /> <span class="sr-only">Not included</span> } @else { <span class="font-mono text-xs">{{ value }}</span> } </td> } </tr> } </tbody> </table> </div> } <ui-separator /> </div> } </div> </div> <button ui-button class="mt-6">Start on Business</button> </div> </section> `, }) export class UiPricingMatrixGroupedComponent { protected readonly Check = Check protected readonly ChevronDown = ChevronDown protected readonly Minus = Minus readonly plans = plans readonly groups = groups @Input() set initialOpen(v: string[] | undefined) { if (v) this.open.set([...v]) } @Input('class') className?: string // Opens on the first group only: a fully expanded matrix is the thing readers // bounce off, and every group is one click from open. readonly open = signal<string[]>([groups[0].id]) get rootClass(): string { return cn('bg-background', this.className) } isOpen(id: string): boolean { return this.open().includes(id) } chevronClass(id: string): string { return cn('text-muted-foreground size-4 shrink-0 transition-transform', !this.isOpen(id) && '-rotate-90') } toggle(id: string): void { this.open.update((current) => (current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id])) } }src/app/components/blocks/pricing-matrix-grouped/index.ts0.1 kB
export { UiPricingMatrixGroupedComponent, type PricingGroupedGroup, type PricingGroupedRow, } from './pricing-matrix-grouped.component'
Raw manifest:https://uipkge.dev/r/angular/pricing-matrix-grouped.json