Skip to main content

PDP Badges

Badges displayed on Product Detail Pages (PDPs) are managed in the hydrogen-storefront repository using React components with Tailwind styling.

Implementationโ€‹

Badge Componentsโ€‹

PDPs use two primary badge components:

ComponentFileBadge Type
PillBadgesapp/components/PillBadges/pillBadges.component.tsxPill Badge (max 2 displayed)
TextBadgeapp/components/TextBadge/textBadge.component.tsxText Badge (status/promo)

Rendering Locationโ€‹

Badges are rendered in the ProductInfo component (app/components/ProductInfo/productInfo.component.tsx):

PDP Badge Layout
ProductInfo Component
โ”œโ”€โ”€ Pill Badges (top of product info) โ† Max 2 badges
โ”œโ”€โ”€ Title & Subtitle
โ”‚ โ”œโ”€โ”€ Text Badge - Out of Stock โ† OR
โ”‚ โ”œโ”€โ”€ Text Badge - First Swatches โ† OR (swatches only)
โ”‚ โ”œโ”€โ”€ Text Badge - Promo โ† (mutually exclusive, 1 displays)
โ”‚ โ””โ”€โ”€ Discontinuation notice โ† (not a badge, plain text)
โ”œโ”€โ”€ Price
โ”‚ โ””โ”€โ”€ Text Badge - Final Sale โ† Conditional (top right)
โ””โ”€โ”€ Reviews, Description...

Badge Display Limitsโ€‹

Badge TypeMax CountLocationBehavior
Pill Badge2Top of product infoTop 2 from priority order
Text Badge (subtitle)1Below subtitleoos/firstswatches/promo (mutually exclusive)
Text Badge (final sale)1Top right (by price)Separate location
Total42 pill + 1 subtitle + 1 final sale

Note: While the code supports multiple subtitle Text Badges, in practice only 1 displays:

  • oos and firstswatches are mutually exclusive (different product types)
  • Swatches won't have promo badges (already free)
  • Out of stock products rarely have promo badges

Text Badgesโ€‹

Text Badges display promotional and status-related information. They render in two distinct locations:

  • Below subtitle: Status and promo badges
  • By price (top right): Final Sale badge

Subtitle Text Badgesโ€‹

Subtitle Text Badges
VariantConditionLabelStyling
oosOut of stock + no waitlist"Out of Stock"Dark mauve text
firstswatchesProduct type is Swatch"Your first 6 swatches are free"Dark mauve text
promoHas matching promo tagDynamicNavy/mauve text

Final Sale Text Badgeโ€‹

VariantConditionLabelStyling
finalsaleHas 'finalsale' tag"Final Sale"Red text (text-brand-errorText)

The Final Sale badge renders in a separate location (top right, next to price) and is independent of the subtitle badges.

Promo Text Badge Logicโ€‹

The getBadgeContent() function determines promo text based on tags (priority order):

PriorityConditionLabel
1'badge:lightningdeal' tag"Extra 30% off with VIRALDEAL"
2'BMSMWP' tag"20% off 4+ with TIEONEON"
3productType is 'Swatch Book'"Your First Swatch Book is Free"
4'BMSMGR' tag"10% off 4+ with SUITELOOKS"
5'badge:limited time color' tag"Limited Time Color"

Note: Promo labels are hardcoded in getBadgeContent(). Update the component when promo codes change.

Usageโ€‹

<TextBadge
variant="promo"
tags={product.tags}
vendor={product.vendor}
productType={product.productType}
/>

Pill Badgesโ€‹

Pill Badges are pill-shaped badges with colored backgrounds. Only the first 2 badges are rendered based on priority order.

Pill Badge Priorityโ€‹

Pill Badges Priority
PriorityBadgeConditionBackground
1EOY Sale'badge:eoy25clearance' tagbg-core-funkyPink-50
2Waived Rush FeerushFee === '0.0'bg-brand-deepBeige-50
3Custom BadgeTag starts with 'custom-badge:'bg-core-funkyPink-50
4Limited Time Offer'badge:limited time offer' tagbg-core-funkyPink-50
5Free Personalization'badge:free personalization' tagbg-brand-deepBeige-50
6PersonalizationisPersonalizable === truebg-brand-deepBeige-50
7DiscontinuedisDiscontinued === truebg-brand-deepBeige-25
8New'New Arrival' tagbg-brand-deepBeige-25
9Best Seller'Best Seller' tagbg-brand-deepBeige-25

Note: Screenshot excludes Custom Badge and EOY Sale badges since they are dynamic/seasonal.

Usageโ€‹

<PillBadges
tags={product.tags}
isDiscontinued={!!discontinuationDate}
isPersonalizable={enablePersonalization}
isGiftCard={isGiftCard}
isSwatch={isSwatch}
isSwatchBook={isSwatchBook}
rushFee={rushFee}
/>

Custom Pill Badgesโ€‹

Products can have dynamic custom badges using the custom-badge: tag prefix:

Tag: "custom-badge:Limited Edition"
Renders: Pill Badge with "Limited Edition" text

Custom Pill Badges are excluded for gift cards, swatches, and swatch books.

Badge Propsโ€‹

TextBadge Propsโ€‹

interface TextBadgeProps {
variant: 'finalsale' | 'promo' | 'firstswatches' | 'oos';
tags: string[];
vendor: string;
productType?: string;
}

PillBadges Propsโ€‹

interface PillBadgesProps {
tags: string[];
isDiscontinued: boolean;
isPersonalizable: boolean;
isGiftCard: boolean;
isSwatch: boolean;
isSwatchBook: boolean;
rushFee?: string;
}

Data Sources (Shopify)โ€‹

Product Tagsโ€‹

Badge triggers from Shopify product tags:

Tag PatternBadge Type
'badge:eoy25clearance'EOY Sale Pill Badge
'finalsale'Final Sale Text Badge
'badge:lightningdeal'Lightning Deal Text Badge
'badge:limited time color'Limited Time Color Text Badge
'badge:limited time offer'Limited Time Offer Pill Badge
'badge:free personalization'Free Personalization Pill Badge
'badge:coming soon'Coming Soon status
'custom-badge:[text]'Custom Pill Badge
'BMSMWP'Bulk purchase Text Badge
'BMSMGR'Bulk purchase Text Badge
'New Arrival'New Arrival Pill Badge
'Best Seller'Best Seller Pill Badge

Product Metafieldsโ€‹

MetafieldNamespacePurpose
discontinuation_datecustomDiscontinued Pill Badge (format: yyyy-MM-dd)
discontinuation_levelcustomDetermines discontinuation scope
enable_personalizationtmsPersonalization Pill Badge
rush_feecustomWaived Rush Fee Pill Badge (when '0.0')

Stylingโ€‹

Text Badge Classesโ€‹

const variantStyles = {
finalsale: 'text-brand-errorText capitalize whitespace-nowrap',
promo: 'text-suits-navy', // or 'text-core-darkerMauve'
firstswatches: 'text-core-darkerMauve whitespace-nowrap',
oos: 'text-core-darkerMauve whitespace-nowrap',
};

Pill Badge Classesโ€‹

Base styles applied to all Pill Badges:

'text-brand-darkerMocha capitalize rounded-[8px] w-fit py-1 px-3 !font-normal text-xs min-h-[26px]'

Background variants:

StyleClassesUsed For
Light beigebg-brand-deepBeige-50Waived Rush Fee, Personalization
Pinkbg-core-funkyPink-50Limited Time Offer, Custom badges
Lighter beigebg-brand-deepBeige-25Discontinued, New, Best Seller

Adding a New Badgeโ€‹

Adding a Text Badge Variantโ€‹

  1. Add variant type in textBadge.component.tsx:

    export type TextBadgeVariant = 'finalsale' | 'promo' | 'newvariant' | ...;
  2. Add styling in the variant styles object:

    newvariant: 'text-your-color whitespace-nowrap',
  3. Add content logic in getBadgeContent() if needed

Adding a Pill Badgeโ€‹

  1. Add condition in pillBadges.component.tsx useMemo hook:

    if (tags.includes('badge:newbadge')) {
    badges.push({
    label: 'New Badge',
    className: 'bg-brand-deepBeige-50',
    });
    }
  2. Position in priority order (badges are added in priority sequence)

Tag-Based Badgeโ€‹

To add a tag-triggered badge:

  1. Define the tag pattern (e.g., 'badge:newfeature')
  2. Add condition check in the appropriate component
  3. Configure Shopify to add the tag to products

Testingโ€‹

Test files are located alongside components:

  • app/components/PillBadges/pillBadges.spec.tsx
  • app/components/TextBadge/textBadge.spec.tsx

Key test scenarios:

  • Maximum 2 Pill Badges displayed
  • Badge priority ordering
  • Correct text/styling per variant
  • Tag matching logic
  • app/components/TextBadge/textBadge.component.tsx - Text Badge component
  • app/components/PillBadges/pillBadges.component.tsx - Pill Badge component
  • app/components/ProductInfo/productInfo.component.tsx - Badge rendering location
  • app/routes/products.$handle.tsx - Product page data fetching
  • app/lib/constants.ts - Badge-related constants

Differences from PLP Badgesโ€‹

AspectPDPPLP
Repositoryhydrogen-storefrontstatic-assets
FrameworkReact/RemixSearchSpring
RenderingClient-side componentsServer-side afterStore hook
Pill BadgesMax 2 (priority system)None
Text Badges2 max (1 subtitle + 1 final sale)1 max (priority system)
Image BadgesNone1 max (same position)
Total max42
StylingTailwind classesSCSS + inline styles