Menu

Change language

GitHub repo

Pricing — Sticky Header Matrix

blockmarketing

Long feature matrix whose plan header row pins to the top while the body scrolls, so the column a checkmark belongs to stays identifiable at any scroll depth.

Installation

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

Variants

Loading interactive previews…

Schema

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

PricingStickyRow
interface PricingStickyRow {
  feature: string
  values: (boolean | string)[]
}

npm dependencies

Used by

Files installed (2)

  • src/app/components/blocks/pricing-matrix-sticky-header/pricing-matrix-sticky-header.component.ts5.4 kB
    // Long feature matrix whose plan header row pins to the top while the body scrolls,
    // so the column a checkmark belongs to stays identifiable at any scroll depth.
    // 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 {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface PricingStickyRow {
      feature: string
      values: (boolean | string)[]
    }
    
    const plans = ['Team', 'Business', 'Enterprise']
    
    // Cells are true / false / a string, so a plan can state a limit rather than
    // only claiming a feature exists.
    const rows: PricingStickyRow[] = [
      { feature: 'Certified metric definitions', values: ['50', '500', 'Unlimited'] },
      { feature: 'Connected warehouses', values: ['1', '3', 'Unlimited'] },
      { feature: 'Query budget per team', values: [false, true, true] },
      { feature: 'Row-level access control', values: [false, true, true] },
      { feature: 'SCIM provisioning', values: [false, true, true] },
      { feature: 'Audit log export', values: [false, '90 days', 'Unlimited'] },
      { feature: 'Period locking', values: [false, true, true] },
      { feature: 'Embedded dashboards', values: [false, true, true] },
      { feature: 'Data residency choice', values: [false, false, true] },
      { feature: 'Customer-managed keys', values: [false, false, true] },
      { feature: 'Private link', values: [false, false, true] },
      { feature: 'Named support engineer', values: [false, false, true] },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-pricing-matrix-sticky-header, [ui-pricing-matrix-sticky-header]',
      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,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <section data-slot="pricing-matrix-sticky-header" [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">Every line, no asterisks</h2>
            <p class="text-muted-foreground mt-3 text-lg">
              Limits differ between plans. Where a plan caps something, the cap is written in the cell.
            </p>
    
            <div ui-card class="mt-8">
              <div ui-card-content class="p-0">
                <!-- The header pins with CSS alone. Thirty rows down, a checkmark is
                     still attributable to a column without scrolling back up. -->
                <div class="max-h-[28rem] overflow-auto">
                  <table ui-table>
                    <thead ui-table-header class="bg-card sticky top-0 z-10">
                      <tr ui-table-row>
                        <th ui-table-head class="bg-card">Capability</th>
                        @for (plan of plans; track plan) {
                          <th ui-table-head class="bg-card text-center">{{ plan }}</th>
                        }
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (row of rows; track row.feature) {
                        <tr ui-table-row>
                          <td ui-table-cell class="font-medium">{{ row.feature }}</td>
                          @for (value of row.values; track $index) {
                            <td ui-table-cell class="text-center">
                              @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>
              </div>
            </div>
    
            <div class="mt-6 flex flex-wrap gap-3">
              <button ui-button>Start on Team</button>
              <button ui-button variant="outline">Talk about Enterprise</button>
            </div>
          </div>
        </section>
      `,
    })
    export class UiPricingMatrixStickyHeaderComponent {
      protected readonly Check = Check
      protected readonly Minus = Minus
    
      readonly plans = plans
      readonly rows = rows
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    }
    
  • src/app/components/blocks/pricing-matrix-sticky-header/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/pricing-matrix-sticky-header.json