Skip to content

FRD-059: Price Comparison Table Widget

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.0.0 (P2)
T-Shirt SizeM
TypeWidget

Document Summary

A pricing-tier comparison table widget that extends the existing ComparisonTable component with pricing-specific features: tier-level pricing display, recommended-tier highlighting, badge support for labels like “Most Popular” or “Best Value,” and a per-tier CTA button. Designed for SaaS pricing pages and plan-selection flows.


Introduction

Overview

SaaS products need a standardized way to present pricing tiers side by side. The existing comparison-table.tsx handles generic feature matrices but lacks pricing headers, CTA buttons per column, and badge affordances. This widget wraps or extends ComparisonTable to add those capabilities.

Goals

  • Display pricing tiers with name, price, billing period, and optional badge.
  • Highlight a recommended tier visually.
  • Render per-tier CTA buttons (e.g., “Start Free Trial,” “Contact Sales”).
  • Reuse the existing ComparisonTable feature-matrix rendering where possible.
  • Ship Storybook stories covering 2-tier, 3-tier, 4-tier, and enterprise layouts.

Non-Goals

  • Billing integration or subscription management.
  • Plan switching logic or upgrade/downgrade flows.
  • Feature-gating or entitlement checks.
  • Responsive mobile accordion layout (follow-up work).

Scope

In Scope

ItemDescription
PriceComparisonTable componentWidget composing ComparisonTable with pricing headers and CTAs
PricingTier typeData shape for each tier column
Badge renderingConfigurable badge text and variant per tier
Recommended highlightVisual emphasis on one tier via recommended: true
Per-tier CTAButton with configurable label and onSelectTier callback
Storybook stories2-tier, 3-tier, 4-tier, with-badge, enterprise-custom

Out of Scope

ItemRationale
Billing API integrationConsumer responsibility
Toggle between monthly/annualCan compose externally with state; may add in follow-up
Mobile accordion viewSeparate enhancement tracked independently

Users and Pain Points

UserPain Point
SaaS marketing teamsBuilding custom pricing tables per product with inconsistent designs
Design-system consumersComparisonTable lacks pricing-specific affordances
End users (buyers)Difficulty comparing plans when layouts differ across products

Definitions

TermDefinition
TierA pricing plan level (e.g., Free, Pro, Enterprise)
Feature matrixGrid of features vs. tiers showing availability
BadgeA small label overlay on a tier header (e.g., “Most Popular”)
CTACall-to-action button at the bottom of each tier column

Current State

comparison-table.tsx exists with ComparisonColumn, ComparisonRow, and ComparisonTableProps interfaces. It renders a TanStack Table-based feature matrix with check/x/text cells and supports a highlighted flag on columns. It does not support pricing headers, badges, or per-column action buttons.


Proposed Solution

Create a PriceComparisonTable widget at src/components/widgets/price-comparison-table.tsx that:

  1. Accepts PricingTier[] which extends ComparisonColumn with price, billingPeriod, badge, ctaLabel, and recommended fields.
  2. Renders a pricing header row above the feature matrix with formatted prices and optional badges.
  3. Delegates feature-row rendering to the existing ComparisonTable component.
  4. Appends a CTA button row below the feature matrix.
  5. Applies visual highlight (elevated background, ring) to the recommended tier column.

Requirements

The widget must compose with ComparisonTable rather than forking it. All pricing display is driven by props; no internal price calculation occurs.


Functional Requirements

IDRequirementPriority
FR-01Render tier columns with name, price, and billing period in the headerMust
FR-02Display an optional badge on any tier headerMust
FR-03Visually highlight the recommended tier with distinct stylingMust
FR-04Render the feature matrix using ComparisonTable rowsMust
FR-05Display a CTA button per tier at the bottom of the tableMust
FR-06Call onSelectTier(tierId) when a CTA is clickedMust
FR-07Support an optional subtitle per tier for additional contextShould
FR-08Allow a custom-tier column (e.g., “Enterprise — Contact Us”) with no priceShould
FR-09Support grouped feature rows via ComparisonRow.groupShould

Non-Functional Requirements

IDRequirement
NFR-01Renders up to 6 tier columns and 40 feature rows without layout issues
NFR-02Full light/dark theme support
NFR-03Bundle addition under 3 KB gzipped (leverages existing ComparisonTable)

API / Interface Requirements

interface PricingTier extends ComparisonColumn {
price?: string; // e.g. "$29", "Custom"
billingPeriod?: string; // e.g. "/mo", "/year"
badge?: string; // e.g. "Most Popular"
badgeVariant?: "default" | "active" | "complete" | "warning";
ctaLabel?: string; // e.g. "Start Free Trial"
ctaVariant?: "default" | "primary" | "ghost";
recommended?: boolean;
}
interface PriceComparisonTableProps {
tiers: PricingTier[];
rows: ComparisonRow[];
title?: string;
description?: string;
onSelectTier?: (tierId: string) => void;
className?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01Table uses proper <table> semantics from ComparisonTable
A11Y-02Recommended tier is conveyed via aria-label not just color
A11Y-03Badge text is accessible to screen readers
A11Y-04CTA buttons have descriptive aria-label including tier name
A11Y-05Column headers use <th scope="col">

Content and Documentation Requirements

  • Storybook doc page with usage guidance, props table, and composition examples.
  • Stories: TwoTiers, ThreeTiers, FourTiers, WithBadges, EnterpriseTier, GroupedFeatures.
  • Migration guide from raw ComparisonTable to PriceComparisonTable.

Dependencies

DependencyTypeNotes
ComparisonTableInternalCore feature-matrix rendering
BadgeInternalTier badges
ButtonInternalPer-tier CTAs
@tanstack/react-tableExternalVia ComparisonTable

Risks and Tradeoffs

RiskImpactMitigation
ComparisonTable API changesBreaks PriceComparisonTablePin to ComparisonTable’s current interface; add integration test
Wide tables on mobilePoor UX on small screensDocument horizontal scroll; plan mobile accordion as follow-up
Badge overuseVisual clutterDocument recommendation: max one badge per table

Open Questions

  1. Should monthly/annual toggle be built in or always composed externally?
  2. Do we need a “Compare selected” mode for selecting two tiers to compare?
  3. Should feature-group headers be sticky on scroll?

Acceptance Criteria

  • PriceComparisonTable renders pricing headers with name, price, period, and badge.
  • Recommended tier has visually distinct styling and an accessible label.
  • CTA buttons call onSelectTier with the correct tier ID.
  • Feature matrix delegates to ComparisonTable and renders correctly.
  • All Storybook stories render without errors.
  • Passes axe accessibility audit with zero violations.
  • Unit tests cover rendering with 2, 3, and 4 tiers, badge display, and CTA callbacks.

LLM Handoff Instructions

When implementing this FRD:

  1. Create src/components/widgets/price-comparison-table.tsx.
  2. Import and compose ComparisonTable from @/components/ui/comparison-table.
  3. Extend ComparisonColumn for the PricingTier type.
  4. Create src/components/widgets/price-comparison-table.stories.tsx with all listed stories.
  5. Create src/components/widgets/price-comparison-table.test.tsx.
  6. The pricing header and CTA footer should wrap the ComparisonTable in a flex layout.
  7. Use Badge for tier badges and Button for CTAs.
  8. Recommended tier highlight: use a ring or elevated background via Tailwind utilities.

Decision Log

DateDecisionRationale
2026-05-26Compose with ComparisonTable rather than forkReduces duplication; inherits table accessibility and theming
2026-05-26Prices are pre-formatted strings, not numbersAvoids locale/currency formatting complexity in the widget

Document History

DateVersionAuthorChanges
2026-05-260.1David HolmesInitial draft