UIPackage
Menu

Framework

Change language

Boilerplate repo

Pricing — Grouped Matrix

blockmarketing

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.

Also available for Vue ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/pricing-matrix-grouped.json
Named registry:npx shadcn@latest add @uipkge-react/pricing-matrix-groupedInstalls to:components/blocks/

Variants

Loading interactive previews…

Files installed (1)

  • components/blocks/PricingMatrixGrouped.tsx5.8 kB
    'use client'
    
    import { useState } from 'react'
    import { Check, ChevronDown, Minus } from 'lucide-react'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent } from '@/components/ui/card'
    import { Separator } from '@/components/ui/separator'
    import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
    
    const plans = ['Team', 'Business', 'Enterprise']
    
    const groups: {
      id: string
      label: string
      summary: string
      rows: { feature: string; values: (boolean | string)[] }[]
    }[] = [
      {
        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] },
        ],
      },
    ]
    
    export function PricingMatrixGrouped() {
      // Opens on the first group only: a fully expanded matrix is the thing readers
      // bounce off, and every group is one click from open.
      const [open, setOpen] = useState<string[]>([groups[0].id])
    
      const toggle = (id: string) =>
        setOpen((current) => (current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id]))
    
      return (
        <section data-slot="pricing-matrix-grouped" className="bg-background">
          <div className="mx-auto max-w-4xl px-6 py-20 lg:py-28">
            <Badge variant="secondary">Compare plans</Badge>
            <h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Grouped, so it stays readable</h2>
    
            <Card className="mt-8">
              <CardContent className="p-0">
                <div className="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>
                  {plans.map((plan) => (
                    <span key={plan} className="text-center">
                      {plan}
                    </span>
                  ))}
                </div>
                <Separator />
    
                {groups.map((group) => (
                  <div key={group.id}>
                    <button
                      type="button"
                      className="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"
                      aria-expanded={open.includes(group.id)}
                      aria-controls={`group-${group.id}`}
                      onClick={() => toggle(group.id)}
                    >
                      <ChevronDown
                        className={`text-muted-foreground size-4 shrink-0 transition-transform ${
                          open.includes(group.id) ? '' : '-rotate-90'
                        }`}
                        aria-hidden="true"
                      />
                      <span className="min-w-0">
                        <span className="block text-sm font-medium">{group.label}</span>
                        <span className="text-muted-foreground block text-xs">{group.summary}</span>
                      </span>
                      <span className="text-muted-foreground ml-auto shrink-0 font-mono text-xs">{group.rows.length}</span>
                    </button>
    
                    {open.includes(group.id) && (
                      <div id={`group-${group.id}`}>
                        <Table>
                          <TableBody>
                            {group.rows.map((row) => (
                              <TableRow key={row.feature}>
                                <TableCell className="pl-11 text-sm">{row.feature}</TableCell>
                                {row.values.map((value, index) => (
                                  <TableCell key={index} className="w-20 text-center sm:w-28">
                                    {value === true && (
                                      <>
                                        <Check className="text-success mx-auto size-4" aria-hidden="true" />
                                        <span className="sr-only">Included</span>
                                      </>
                                    )}
                                    {value === false && (
                                      <>
                                        <Minus className="text-muted-foreground/50 mx-auto size-4" aria-hidden="true" />
                                        <span className="sr-only">Not included</span>
                                      </>
                                    )}
                                    {typeof value === 'string' && <span className="font-mono text-xs">{value}</span>}
                                  </TableCell>
                                ))}
                              </TableRow>
                            ))}
                          </TableBody>
                        </Table>
                      </div>
                    )}
                    <Separator />
                  </div>
                ))}
              </CardContent>
            </Card>
    
            <Button className="mt-6">Start on Business</Button>
          </div>
        </section>
      )
    }
    

Raw manifest:https://uipkge.dev/r/react/pricing-matrix-grouped.json