Design System
Components

ProductCard

The card-features card in three types — package, product and category — with a tinted panel, a discount or colour swatches, and a price.

Figma description · card-features-packageOpen in Figma ↗

Use for: Named plans, bundles, or subscription tiers where partner or app inclusions need to be surfaced alongside the package name.

Not for: Physical or digital products — use card-features-product. Category entry points — use card-features-category. Service add-ons — use card-features-addon.

Choosing: Set logoRow=true when the package includes bundled partner services with logos to show; logoRow=false when there are none. Set badgeOrColor=true when there is an active offer badge to show in the footer; badgeOrColor=false when there is none. Set label=true on the price sub-component for a starting price; label=false for a fixed price. Set priceMonthly=true for recurring prices; priceMonthly=false for one-time. Use size=sm on the logo-row to match this card's compact header. Set direction=rtl for Arabic.

Rules: The overline and title use dir=auto — do not override direction on the text layers. The logo-row sits at the bottom of the header block — do not move it to the footer. The badge sits in the .product-card-badge-or-color-selection slot in the footer — do not reposition it. Card is fixed at 229×300px — do not resize.

smilessmiles+4
iPhone 15 Clear Case
20% off
From
AED 99
Props
import SwiftUI
import EandTokens

// card-features type=product: 229×300 (row-4), r20, raised white. A 144pt tint panel
// (r24, 4pt white inside ring) holds a 112pt image. Footer (8/12/12, gap 4): heading/sm
// title, lg badge, then the body/xl price under a body/md "from". The tint colours the
// panel only, never the card.
struct ProductCard: View {
  let title: String; let price: String
  var body: some View {
    VStack(alignment: .leading, spacing: 0) {
      Image("case").resizable().scaledToFit().frame(height: 112)
        .frame(maxWidth: .infinity, minHeight: EandScale.scale144)
        .background(EandColor.atomSurfacesBlue)
        .overlay(RoundedRectangle(cornerRadius: EandRadius.radius8)
          .strokeBorder(EandColor.borderInteractiveInversePressed, lineWidth: EandBorder.xl))
        .clipShape(RoundedRectangle(cornerRadius: EandRadius.radius8))
      VStack(alignment: .leading, spacing: EandSpacing.xs) {
        Text(title)
          .font(.system(size: EandTypography.headingSm.size, weight: EandTypography.headingSm.weight))
          .foregroundColor(EandColor.midnight1100)   // legacy High-emphasis #140f21 (no semantic token)
          .lineLimit(2)
        Spacer(minLength: 0)
        OfferBadge(text: "20% off", size: .lg)
        Text("from").font(.system(size: EandTypography.bodyMd.size))
          .foregroundColor(EandColor.textDefaultSubtle)
        Text(price).font(.system(size: EandTypography.bodyXl.size))
      }
      .padding(.top, EandSpacing.sm).padding([.horizontal, .bottom], EandSpacing.md)
    }
    .frame(width: 229, height: EandSection.bodyHeightRow4)
    .background(EandColor.surfaceRaisedDefault)
    .clipShape(RoundedRectangle(cornerRadius: EandRadius.radius7))
  }
}

When to use

  • Use for packages and add-ons, products such as devices and accessories, and shop categories, in a Section of size lg.
  • Don't use for plans (use PlanCard) or for deals (use DealCard).

Choosing a type

typeTinted panelFooter
package176px: eyebrow, title (up to four lines), logo rowDiscount or swatches, price
product144px: a 112px image, with the logo row over ittitle (two lines), discount or swatches, price
categoryFills the card: title (one line), a 128px image, the Discount badge and the price—

Without a type, a card with an image is a product and one without is a package.

Anatomy

  • Tint — tint colours the panel only; the card itself is always white. Pass a .card-bg-color name (default, red, orange, yellow, green, cyan, blue, violet) or any CSS colour.
  • Discount or swatches — discount renders the lg Discount badge; colors renders 12px colour spots in its place.
  • Price — a label, then price and period in body/xl. priceLabel is from (default), pay-only, smiles-combined (with labelPts, "From 30 PTS") or none. pts shows the price in Smiles points.
  • Logos — logos adds the small logo row on package and product cards; true shows placeholders.
<ProductCard type="package" eyebrow="Add-on" title="Unlimited social data" discount="20% off" price="AED 35" />
<ProductCard type="product" image={<img src={phone} alt="" />} title="iPhone 16" colors={['#4fa0f8', '#b3b3b3']} priceLabel="pay-only" />

Deprecated and changed

type="addon" is deprecated; it is the V1.1 name of package. tint used to colour the whole card and now colours the panel only.

Props

PropTypeDefaultDescription
colors?string[]—Figma badge-or-color=color-spot — swatches (CSS colours) shown in place of the badge.
discount?ReactNode—Figma badge-or-color=badge — the lg Discount badge label, e.g. "20% off".
eyebrow?ReactNode—Figma overline-string (package only).
image?ReactNode—Media for product / category.
labelPts?ReactNode30Points for priceLabel="smiles-combined" ("From {n} PTS").
logos?boolean | ReactNode[]—Figma logo-row (sm). true shows placeholders; package and product only.
period?ReactNode/moFigma price-monthly. null hides it.
price?ReactNodeAED 200Figma price-string.
priceLabel?'none' | 'from' | 'pay-only' | 'smiles-combined'fromFigma .product-card-price-label. Default from.
pts?boolean—Figma .product-card-price type=smiles: Smiley + amount + "PTS".
tint?(string & {}) | CardBgTint—A .card-bg-color tint name (Figma 25710:20065) for the panel. A raw CSS colour still works.
title?ReactNodeiPhone Clear Case For Safe UseFigma title-string.
type?'package' | 'product' | 'category' | 'addon'—Which card-features sibling this is. addon is the V1.1 name of package.
width?string | number229

Design

Figma source: card-features-package — etisalat Consumer App DSL V1.2 · card-features-product · card-features-category

  • 229 × 300 (section/body/height/row-4) at a 20px radius (border-radius/7) on surface/raised/default.
  • The tint panel has a 24px radius (border-radius/8) and a 4px inside ring in border/interactive/inverse/pressed (white).
  • Footer padding 8/12/12 (spacing/sm, spacing/md), gap 4, bottom-aligned.
  • Titles apply the heading text styles: heading/sm (18px, 110%) on package and product, heading/md (20px) on category. The price is body/xl (20px Regular) under a body/md subtle label.
  • The product title binds a legacy ink (#140f21) that has no V1.2 token, so it is a literal.

On this page