Skip to content

FRD: Product Card

Document Summary

FieldDetails
Feature NameProduct Card
StatusDraft
OwnerDavid Holmes
ContributorsDesign, Engineering
Target Releasev2.0.0 (P2)
Related LinksRoadmap item #57
Last Updated2026-05-26

Introduction

Overview

Product Card is an e-commerce widget that displays a product with image, name, price, optional badge (e.g., “Sale”, “New”), and an add-to-cart CTA. It is designed for SaaS applications with marketplace, catalog, or storefront features. The existing PricingCard handles subscription plan presentation; Product Card focuses on physical or digital product display with purchase intent. The widget is intentionally compact (size S) and composable.

Goals

  • Deliver a <ProductCard> component that displays a product image, name, price, optional badge, and add-to-cart button.
  • Use design-system tokens for all visual properties.
  • Support variant states: default, on-sale, out-of-stock, featured.
  • Ship Storybook stories with realistic e-commerce data.

Non-Goals

  • Full product detail page layout.
  • Cart management or checkout flow.
  • Product search or filtering.
  • Image gallery or zoom.
  • Reviews or ratings display (separate component).

Scope

In Scope

AreaDescription
Component<ProductCard> displaying product image, name, price, badge, and CTA
ImageProduct image with aspect-ratio container and fallback placeholder
NameProduct name with optional truncation
PriceCurrent price with optional original price (strikethrough for sales)
BadgeOptional status badge (“Sale”, “New”, “Sold Out”, custom)
CTAAdd-to-cart button with loading and disabled states
VariantsDefault, on-sale (with original price), out-of-stock (disabled CTA), featured (highlighted border)
StoriesStorybook stories for all variants and states

Out of Scope

AreaReason
Product detail pageSeparate layout pattern
Cart managementApplication-layer concern
Image gallery/zoomSeparate component
Reviews/ratingsSeparate component
Product comparisonSeparate widget

Users and Pain Points

User Groups

UserDescriptionNeeds
DevelopersEngineers building e-commerce or marketplace featuresA composable product card using DS primitives
DesignersDesign-system consumersConsistent product display aligned with DS tokens
End UsersShoppers browsing productsClear product info with easy add-to-cart action

Pain Points

UserPain PointImpact
DevelopersBuilding product cards from scratch with image containers, price formatting, badge placement, and cart button stateRepeated boilerplate, inconsistent cards across products
End UsersInconsistent product cards make it hard to scan and compare productsSlower browsing, reduced purchase intent

Definitions

TermDefinition
Product CardA compact display widget showing a product’s key information and purchase action
Original PriceThe pre-discount price shown with strikethrough styling during sales
CTACall to action; in this context, the “Add to Cart” button
FeaturedA visual variant with a highlighted border or background to draw attention

Current State

Existing Behavior

PricingCard in src/components/ui/cards/pricing-card.tsx displays a subscription pricing tier with plan name, price, billing period, feature list, and CTA. It is designed for pricing pages, not product catalogs. There is no product-focused card for e-commerce use cases.

Current Limitations

  • No e-commerce product card component.
  • PricingCard is subscription-focused and does not support product images, sale prices, or stock status.
  • No pattern for original/sale price display with strikethrough.

Existing Workarounds

  • Developers build custom product cards with ad hoc image containers and inline styles.
  • Sale-price display requires manual strikethrough styling.

Proposed Solution

Summary

Introduce <ProductCard> that accepts product data (image, name, price, originalPrice, badge, onAddToCart) and renders a compact card. The image fills an aspect-ratio container at the top. Below it, the product name, price (with optional strikethrough original price), and an optional badge display. At the bottom, an “Add to Cart” button fires the onAddToCart callback. Out-of-stock products show a disabled button with “Sold Out” text.

Key Capabilities

  • Product image with configurable aspect ratio and placeholder fallback.
  • Product name with two-line truncation.
  • Current price display with optional original price (strikethrough) for sales.
  • Configurable badge (text and tone).
  • Add-to-cart button with loading state and disabled state for out-of-stock.
  • Featured variant with highlighted border.
  • Hover elevation animation.

User Experience

Users see a grid of product cards. Each card shows a product image, name, price, and an “Add to Cart” button. Sale items show the original price crossed out next to the sale price, with a “Sale” badge. Out-of-stock items show a “Sold Out” badge and a disabled button. Hovering a card slightly elevates it.

Developer Experience

<ProductCard
image="/products/headphones.jpg"
name="Wireless Noise-Cancelling Headphones"
price="$249.99"
originalPrice="$329.99"
badge={{ text: "Sale", tone: "destructive" }}
onAddToCart={() => addToCart("headphones-1")}
/>

Requirements

IDRequirementPriorityNotes
FR-001ProductCard displays product image, name, and priceMust-
FR-002Optional original price renders with strikethroughMust-
FR-003Optional badge renders with configurable text and toneMust-
FR-004Add-to-cart button fires onAddToCart callbackMust-
FR-005Out-of-stock variant disables the CTAMust-
FR-006Featured variant applies highlighted borderShould-
FR-007Image placeholder renders when image is not providedShould-

Priority Definitions

PriorityMeaning
MustRequired for this feature to ship.
ShouldImportant, but can be deferred if needed.
CouldNice to have. Not required for initial release.

Functional Requirements

IDRequirementUser BenefitPriority
FUNC-001image prop renders in an aspect-ratio container (default 4:3)Consistent image displayMust
FUNC-002When image is not provided, a placeholder with a product icon rendersGraceful fallbackShould
FUNC-003name renders with two-line truncation (line-clamp-2)Consistent card heightsMust
FUNC-004price renders prominently; originalPrice renders with line-through stylingClear pricingMust
FUNC-005badge prop accepts { text: string; tone?: string } and renders a BadgeFlexible status indicationMust
FUNC-006onAddToCart fires when the CTA button is clickedCart integrationMust
FUNC-007isAddingToCart prop shows loading spinner on the CTA buttonFeedback during async addShould
FUNC-008outOfStock prop disables CTA and changes button text to “Sold Out”Clear unavailabilityMust
FUNC-009featured prop adds a highlighted border or ringVisual emphasisShould
FUNC-010Card has hover elevation animationInteractive feelShould

Non-Functional Requirements

IDRequirementCategoryPriority
NFR-001Component renders in under 10msPerformanceMust
NFR-002Image lazy-loads with loading="lazy"PerformanceShould
NFR-003All interactive elements are keyboard-navigableAccessibilityMust
NFR-004Works in light and dark themesThemingMust
NFR-005No new runtime dependenciesMaintainabilityMust
NFR-006Image container does not cause layout shift on loadPerformanceMust

API / Interface Requirements

Public API

NameTypeDescriptionRequired
namestringProduct nameYes
pricestringDisplay price (pre-formatted)Yes
imagestringProduct image URLNo
imageAltstringImage alt textNo (defaults to name)
originalPricestringOriginal price (shown with strikethrough)No
badge{ text: string; tone?: string }Status badge configNo
onAddToCart() => voidAdd-to-cart callbackNo
isAddingToCartbooleanLoading state for CTANo
outOfStockbooleanDisable CTA with “Sold Out” textNo
featuredbooleanHighlighted variantNo
aspectRatiostringImage aspect ratioNo (default: "4/3")
ctaLabelstringCustom CTA button textNo (default: "Add to Cart")
classNamestringAdditional CSS classesNo

Example Usage

import { ProductCard } from "@/components/ui/product-card";
<div className="grid grid-cols-3 gap-4">
<ProductCard
image="/products/headphones.jpg"
name="Wireless Noise-Cancelling Headphones"
price="$249.99"
originalPrice="$329.99"
badge={{ text: "Sale", tone: "destructive" }}
onAddToCart={() => addToCart("sku-001")}
/>
<ProductCard
image="/products/speaker.jpg"
name="Portable Bluetooth Speaker"
price="$79.99"
badge={{ text: "New", tone: "info" }}
onAddToCart={() => addToCart("sku-002")}
/>
<ProductCard
name="Limited Edition Earbuds"
price="$149.99"
outOfStock
/>
</div>

API Notes

  • price and originalPrice are pre-formatted strings; the component does not handle currency formatting.
  • badge.tone maps to DS Badge tones (success, warning, destructive, info, default).
  • aspectRatio uses CSS aspect-ratio property.
  • onAddToCart is optional; cards without it render without a CTA (display-only mode).

Accessibility Requirements

IDRequirementNotes
A11Y-001Product image has meaningful alt text (defaults to product name)-
A11Y-002Price and original price are conveyed to screen readersaria-label on price region: “Price 249.99,originally249.99, originally 329.99”
A11Y-003Badge text is accessible (not just visual)Badge component handles this
A11Y-004Add-to-cart button has descriptive label including product namee.g., “Add Wireless Headphones to cart”
A11Y-005Out-of-stock state conveyed to screen readersaria-disabled, “Sold Out” text
A11Y-006Card is not a focusable link (CTA is the interactive element)Avoids nested interactive problem

Checklist

  • Keyboard support is defined.
  • Focus behavior is defined.
  • Screen reader behavior is defined.
  • Color contrast requirements are met.
  • Reduced motion behavior is considered.
  • Semantic HTML expectations are documented.
  • ARIA usage is defined only where needed.

Content and Documentation Requirements

IDRequirementLocationPriority
DOC-001Storybook docs page with overview and props tableStorybookMust
DOC-002”When to use / When not to use” guidance (vs. PricingCard)Storybook docsMust
DOC-003Stories for default, on-sale, out-of-stock, featured, and no-image variantsStorybookMust
DOC-004Grid layout example story showing multiple cardsStorybookMust

Dependencies

DependencyTypeOwnerStatusNotes
BadgeEngineeringDesign SystemReadyStatus badges
ButtonEngineeringDesign SystemReadyAdd-to-cart CTA
Design tokensDesignDesign SystemReadyColor, spacing, typography, elevation
PricingCardReferenceDesign SystemReadyReference for card shell pattern (not composed directly)

Risks and Tradeoffs

Risk / TradeoffImpactMitigation
Pre-formatted price strings do not support localizationPrices may display incorrectly for different localesDocument that consumer handles currency formatting; consider adding a formatPrice utility in the future
No product link/href propCards cannot be clickable links to product pagesAdd optional href prop that wraps the card in an anchor; or let consumer wrap externally
Two-line name truncation may cut off important informationProduct names may be unclearConsumer should provide concise names; tooltip on hover shows full name

Open Questions

IDQuestionOwnerStatusResolution
Q-001Should the card support an href prop to make the entire card a link?David HolmesOpen
Q-002Should a quantity selector be included alongside the add-to-cart button?David HolmesOpen
Q-003Should the card support a “quick view” action that opens a detail overlay?David HolmesOpen

Acceptance Criteria

IDCriteriaRelated Requirement
AC-001Product image renders in an aspect-ratio containerFUNC-001
AC-002Image placeholder renders when image is not providedFR-007, FUNC-002
AC-003Product name renders with two-line truncationFUNC-003
AC-004Price and original price (strikethrough) render correctlyFR-001, FR-002, FUNC-004
AC-005Badge renders with correct text and toneFR-003, FUNC-005
AC-006Add-to-cart button fires onAddToCartFR-004, FUNC-006
AC-007Out-of-stock variant shows disabled “Sold Out” buttonFR-005, FUNC-008
AC-008Featured variant applies highlighted borderFR-006, FUNC-009
AC-009Loading state shows spinner on CTA buttonFUNC-007
AC-010All Storybook stories render without errorsDOC-003
AC-011Component passes axe accessibility auditNFR-003

LLM Handoff Instructions

Expected LLM Behavior

  • Follow the requirements and acceptance criteria in this document.
  • Do not expand scope beyond the In Scope section.
  • Respect the Out of Scope section.
  • Use Badge for status badges, Button for the add-to-cart CTA.
  • Use CSS aspect-ratio for the image container.
  • Use line-clamp-2 for product name truncation.
  • Use line-through styling for original price.
  • Reference PricingCard for card shell styling patterns (rounded corners, border, padding) but do not extend it.
  • Place stories under the SaaS Widgets Storybook section.

LLM Should Not

  • Build a full product detail page.
  • Add cart management logic.
  • Add a quantity selector.
  • Modify existing PricingCard or Badge components.
  • Add currency formatting logic (prices are pre-formatted strings).

Decision Log

DateDecisionReasonOwner
2026-05-26Separate ProductCard from PricingCardDifferent use cases: e-commerce products vs. subscription plans; different data shapesDavid Holmes
2026-05-26Pre-formatted price strings rather than number + currencyKeeps the component simple; currency formatting varies by locale and is consumer responsibilityDavid Holmes
2026-05-26No href prop in v1Keeps interactive pattern simple (button only); wrapping in anchor can be done by consumerDavid Holmes

Document History

DateAuthorChange
2026-05-26David HolmesInitial draft