ProductCard
The card-features card in three types — package, product and category — with a tinted panel, a discount or colour swatches, and a price.
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.
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
type | Tinted panel | Footer |
|---|---|---|
package | 176px: eyebrow, title (up to four lines), logo row | Discount or swatches, price |
product | 144px: a 112px image, with the logo row over it | title (two lines), discount or swatches, price |
category | Fills 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 —
tintcolours the panel only; the card itself is always white. Pass a.card-bg-colorname (default,red,orange,yellow,green,cyan,blue,violet) or any CSS colour. - Discount or swatches —
discountrenders thelgDiscount badge;colorsrenders 12px colour spots in its place. - Price — a label, then
priceandperiodinbody/xl.priceLabelisfrom(default),pay-only,smiles-combined(withlabelPts, "From 30 PTS") ornone.ptsshows the price in Smiles points. - Logos —
logosadds the small logo row onpackageandproductcards;trueshows 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
| Prop | Type | Default | Description |
|---|---|---|---|
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? | ReactNode | 30 | Points for priceLabel="smiles-combined" ("From {n} PTS"). |
logos? | boolean | ReactNode[] | — | Figma logo-row (sm). true shows placeholders; package and product only. |
period? | ReactNode | /mo | Figma price-monthly. null hides it. |
price? | ReactNode | AED 200 | Figma price-string. |
priceLabel? | 'none' | 'from' | 'pay-only' | 'smiles-combined' | from | Figma .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? | ReactNode | iPhone Clear Case For Safe Use | Figma title-string. |
type? | 'package' | 'product' | 'category' | 'addon' | — | Which card-features sibling this is. addon is the V1.1 name of package. |
width? | string | number | 229 |
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) onsurface/raised/default. - The tint panel has a 24px radius (
border-radius/8) and a 4px inside ring inborder/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 isbody/xl(20px Regular) under abody/mdsubtle label. - The product title binds a legacy ink (#140f21) that has no V1.2 token, so it is a literal.