Compare-Table Compact Row Layout Spec
Context
BUY-2695: Prototype compare-table compact row layout
AI agents comparing products frequently encounter partial merchant coverage — some retailers don't list certain products, have blocked scrapers, or have prices that fluctuate rapidly. The compare-table UI must remain readable and trustworthy even when 30–50% of expected merchant data is absent.
1. Row Anatomy
1.1 Standard Row (Full Data)
┌─────────────────────────────────────────────────────────────────────────────┐
│ [ATTR LABEL] │ [MERCHANT A] │ [MERCHANT B] │ [MERCHANT C] │
│ │ $49.90 SGD │ $52.00 SGD │ — │
└─────────────────────────────────────────────────────────────────────────────┘
- Attribute label column (fixed left): 120px min-width, left-aligned
- Merchant value columns (fluid): equal width distribution, content centered
- Row height: 48px standard, 56px when content wraps
1.2 Compact Row Variants
Price Row (Most Important — Always Prominent)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Price ★ │ $49.90 SGD ▼ │ $52.00 SGD │ — │
│ (48px) │ CHEAPEST │ │ [greyed "—"] │
└─────────────────────────────────────────────────────────────────────────────┘
Availability Row
┌─────────────────────────────────────────────────────────────────────────────┐
│ Stock │ ● In Stock │ ○ Low Stock │ — unavailable │
└─────────────────────────────────────────────────────────────────────────────┘
Rating Row
┌─────────────────────────────────────────────────────────────────────────────┐
│ Rating │ ★★★★☆ (142) │ ★★★★★ (89) │ — │
└─────────────────────────────────────────────────────────────────────────────┘
2. Missing-Data Cell States
2.1 Visual State Matrix
| State | Visual Treatment | CSS Class | Example |
|---|---|---|---|
| Data present | Normal display | .cell--present | $49.90 SGD |
| Price missing | Em-dash + grey text | .cell--missing | — |
| Zero price | Shows "$0.00" with free badge | .cell--zero | $0.00 FREE |
| Stale data | Amber tint + ⚠ badge | .cell--stale | $49.90 ⚠ 8d |
| Very stale | Red tint + ⚠ badge | .cell--very-stale | $49.90 ⚠ 35d |
| Merchant error | Red border + error icon | .cell--error | ⚠ unavailable |
2.2 Missing Price States
Price Missing (retailer doesn't list product)
Merchant C
— unavailable
- Grey italic text
- Tooltip: "Retailer does not carry this product"
Price Missing (scrape error / timeout)
Merchant C
— timeout
- Grey text with clock icon
- Tooltip: "Price temporarily unavailable. [Refresh]"
Price Missing (price not disclosed by retailer)
Merchant C
— no price
- Grey text with lock icon
- Tooltip: "Retailer does not disclose prices"
2.3 Data Freshness Indicators
Appended to cell value when data_freshness is set:
| Freshness | Badge | Color | Example |
|---|---|---|---|
fresh (<24h) | none | default | $49.90 |
recent (24h–7d) | none | default | $49.90 |
stale (7–30d) | ⚠ Xd | amber | $49.90 ⚠ 12d |
very_stale (>30d) | ⚠ Xd | red | $49.90 ⚠ 45d |
3. Partial Coverage Row Behavior
3.1 Column Collapse vs. Show-Gap
When some merchants don't have data for a product, two approaches:
Option A: Show-Gap (Preferred)
- All merchant columns remain visible
- Missing cells show placeholder state
- Row label shows "×3 merchants" coverage indicator
Option B: Column Collapse
- Only merchants with data are shown
- Coverage banner: "Showing 2 of 4 retailers"
Recommendation: Use Option A (show-gap) for AI agent contexts where the user needs to see the full merchant landscape. Use Option B for end-user retail contexts where empty columns create noise.
3.2 Coverage Indicator
Append to row label when not all merchants have data:
Price ★ (3/4 merchants)
Stock (2/4 merchants) ← amber indicator
Rating (4/4 merchants)
Format: (present_count/total_count) appended to label
3.3 "No Data" Row Suppression
For spec rows where only 1 merchant has data:
- Row is demoted to secondary prominence
- Shown with amber "Limited data" badge
- Moved below primary spec rows
4. Compact Row CSS Layout
4.1 Grid Structure
.compare-table {
display: grid;
grid-template-columns: 120px repeat(var(--merchant-count), 1fr);
/* Label column fixed, merchant columns fluid and equal */
}
.row--price {
grid-column: 1 / -1; /* Price spans full width visually */
/* But data still aligned to merchant columns */
}
.row--price .attr-label {
grid-column: 1;
}
.row--price .value-cell {
grid-column: span 1; /* Each value in its merchant column */
}
4.2 Compact Mode (Toggle)
When compact=true:
.compare-table--compact {
--row-height: 36px;
--label-width: 80px;
font-size: 13px;
}
.compare-table--compact .value-cell {
padding: 4px 8px; /* Reduced padding */
}
4.3 Mobile Collapsed View
On screens < 640px:
- Table becomes card stack
- Each merchant becomes a horizontal card
- Price prominently displayed at top of each card
5. Row Priority Order
Display rows in this priority order (top = highest):
- Price — always first, always prominent
- Availability — in-stock status
- Rating — aggregate rating + count
- Shipping — delivery time/cost if available
- Trust badges — verified authentic, secure checkout, returns
- Merchant name — source attribution
- Brand — brand when not in product name
- Specs — product-specific attributes (color, size, storage, etc.)
- URL link — CTA to purchase
Rows 1–4 are primary (always shown). Rows 5–9 are secondary (collapsible on mobile, or hidden behind "Show more specs").
6. Empty / Low-Coverage Behavior
6.1 Full Empty State (0 merchants)
Show LowCoverageState component (already exists at /components/LowCoverageState.tsx).
6.2 Low Coverage State (1–2 merchants)
┌──────────────────────────────────────────────────────────────┐
│ ⚠ Limited price data │
│ Showing prices from 2 retailers. [Suggest a retailer] │
└──────────────────────────────────────────────────────────────┘
Display the 1–2 available merchants with full data. Show coverage indicator on each row.
6.3 Partial Coverage with Stale Data
When >50% of visible data is stale:
┌──────────────────────────────────────────────────────────────┐
│ ⚠ Some prices are outdated │
│ Last verified 12 days ago for 2 retailers. [Refresh all] │
└──────────────────────────────────────────────────────────────┘
7. Handoff Notes for Implementation
7.1 Data Shape
The component receives CompareMatch[] with these key fields for row rendering:
interface CompareRowData {
merchantId: string;
merchantName: string;
price: Decimal | null;
priceMissing: boolean;
priceMissingReason?: 'retailer_api_error' | 'retailer_timeout' | 'product_unavailable' | 'price_not_disclosed' | 'crawl_error';
dataFreshness?: 'fresh' | 'recent' | 'stale' | 'very_stale';
isAvailable: boolean;
rating?: number;
reviewCount?: number;
affiliateUrl?: string;
buyUrl: string;
}
7.2 Accessibility
- All missing cells have
aria-labelwith explicit reason: "Price unavailable from [merchant]" - Stale indicators:
aria-label="Price verified X days ago" - Coverage indicators:
aria-label="Showing 3 of 4 merchants" - Color is not sole indicator — always pair with icon or text
7.3 Performance
- Use CSS
content-visibility: autofor rows outside viewport - Lazy-load merchant images below fold
- Debounce refresh actions (500ms)
7.4 State Management
Reference hooks/useCompareCardState.ts pattern from docs/compact-compare-card-state.md for selection state. This layout spec does not change state management — only visual row structure.
8. Out of Scope
- Selection/checkbox behavior (handled by
ProductComparison.tsx) - Add-to-cart or direct purchase flows
- Sorting or filtering UI
- Export functionality
9. Related Documents
docs/compact-compare-card-state.md— state machine for compare card selectiondocs/compare-surface-microcopy.md— all microcopy strings for compare surfacesdocs/DESIGN_TRUST_BADGES_AND_FRESHNESS.md— trust and freshness badge specsapp/schemas/product.py—CompareMatch,PriceMissingReasonschema definitions
Spec version: 1.0 | Created for BUY-2695 | Frontend handoff reference