Design System
Components

PlanCard

The plans card — an overline, the plan name, partner logos, a Discount badge and the price.

Figma description · plans-portableOpen in Figma ↗

Use for

A compact plan card used to present a named plan or subscription tier with its price, bundled service logos, and an optional promotional badge. Use in contexts where plan information needs to be surfaced at a glance alongside other content — carousel rows, comparison panels, or summary screens.

Not for

Full plan detail pages or primary plan selection flows — use a full-size plan card for those. Promotional or offer-only content with no plan name or price — use a card-features or offer component instead.

Choosing

Set color-scheme=default for light surface contexts. Set color-scheme=inverse for dark surface contexts — the card background switches to surface/base/midnight and all text and badge tokens invert accordingly. Do not apply color-scheme=inverse on a light screen background; ensure the surrounding context is dark. Background color is content-driven, not status-driven — any named color can be used for any plan; there is no fixed color assignment per plan type.

Set services=on when the plan has bundled add-ons to surface via the logo-row. Set services=off when there are no bundled services — the logo-row slot collapses. Set badge=on when there is an active promotional offer (Discount or New) to highlight. Set badge=off when there is no offer. The badge color contrasts with the card background — a red badge on light cards, a light badge on dark cards. Do not override badge color manually; it is set by the color-scheme property.

For the logo-row, use size=md (40px) to match this card's standard context. Toggle individual logo slots (logo1–logo5) to match the number of actual partner logos. Enable xMore only when logos exceed the visible slots; the overflow pill must reflect the actual count of hidden logos. Maximum 3 logos visible before overflow. Set direction=rtl for Arabic.

Rules

The plan label sits top-left using body/md (14px regular) with dir=auto. The plan title sits below it using heading/md (24px bold) with dir=auto, max 2 lines, wraps allowed. Do not truncate the title to a single line. The logo-row sits in the header, 8px below the title. The price sits at the bottom of the card using title/md (20px semi-bold). The "from" label above the price uses body/md subtle — show it when the price is a starting price, hide it otherwise. The badge sits above the price row in the footer. Card is fixed at 222×300px. Do not resize the instance. For RTL, set direction=rtl at the component level — all text fields use dir=auto and will flip automatically.

Postpaid
Freedom Live 200
smilessmiles+4
20% off
from
AED 200/mo
Props
import SwiftUI
import EandTokens

// plans-portable: 222×300 (row-4), r16, padding 16, white. Header (gap 4): overline,
// 20pt heading/md title, logo row md. Footer pinned to the bottom: lg Discount badge,
// "from", then the title/md price.
struct PlanCard: View {
  var body: some View {
    VStack(alignment: .leading, spacing: EandSpacing.xs) {
      Text("Postpaid").font(.system(size: EandTypography.bodyMd.size))
        .foregroundColor(EandColor.textDefaultSubtle)
      Text("Freedom Live 200")
        .font(.system(size: EandTypography.headingMd.size, weight: EandTypography.headingMd.weight))
        .lineLimit(2)
      LogoRow(logos: [Image("brand-smiles")], more: 4).padding(.top, EandSpacing.sm)
      Spacer(minLength: 0)
      OfferBadge(text: "Discount", size: .lg)            // red, 24pt
      Text("from").font(.system(size: EandTypography.bodyMd.size))
        .foregroundColor(EandColor.textDefaultSubtle)
      Text("AED 200/mo")
        .font(.system(size: EandTypography.titleMd.size, weight: EandTypography.titleMd.weight))
    }
    .foregroundColor(EandColor.textDefaultDefault)
    .padding(EandSpacing.lg)
    .frame(width: 222, height: EandSection.bodyHeightRow4, alignment: .topLeading)
    .background(EandColor.surfaceBaseInverse)
    .clipShape(RoundedRectangle(cornerRadius: EandRadius.radius6))
  }
}

When to use

  • Use to compare plans in a carousel, in a Section of size lg.
  • Don't use for products, devices or add-ons (use ProductCard), or for deals with a like count (use DealCard).

Anatomy

  • Header — category, the overline, in body/md subtle; name in bold 20px, up to two lines; then the logo row of 32px partner logos with a +N bubble. Pass logos and moreLogos, or leave smiles on for placeholders.
  • Footer — bottom-aligned: the lg Discount badge (discount={true} labels it "Discount"), the "from" line (from), then price and period.

category and period accept null to hide them.

colorScheme="default" is the white card; colorScheme="inverse" is the midnight card with white ink. Both use the same red Discount badge.

<PlanCard name="Entertainment plans" category="Postpaid" price="AED 1250" period="/mo" discount="20% off" />
<PlanCard colorScheme="inverse" name="Data plans" discount={false} from={false} price="AED 300" />

Deprecated

variant is now colorScheme: midnight becomes inverse. brand has no Figma counterpart.

Props

PropTypeDefaultDescription
category?ReactNodePostpaidFigma overline-string. null hides the overline.
colorScheme?'default' | 'inverse'—Figma color-scheme axis. Takes precedence over variant.
discount?ReactNodetrueFigma badge — the lg Discount badge in the footer. true labels it "Discount".
from?booleantrueFigma price-from — the "from" line above the price.
logos?ReactNode[]—Logos for the logo row; implies smiles.
moreLogos?number—The +N bubble at the end of the logo row. Default 4 with placeholders.
name?ReactNodeEntertainment plansFigma title-string.
period?ReactNode/moFigma price-monthly. null hides it.
price?ReactNodeAED 1250Figma price-string.
smiles?booleantrueFigma logo-row (md, 32px logos). Shows Smiles placeholders unless logos is given.
variant?'default' | 'brand' | 'midnight'—Deprecated. Use colorScheme.
width?string | number222

Design

Figma source: plans-portable — etisalat Consumer App DSL V1.2

  • 222 × 300 (section/body/height/row-4) at a 16px radius (border-radius/6), padding 16 (spacing/lg), on surface/base/inverse or surface/base/midnight.
  • Header and footer gaps are 4 (spacing/xs); the logo row has 8px above it (spacing/sm).
  • The name applies the heading/md text style: 20px at 110%. The price is title/md.
  • Inverse ink binds text/default/inverse and text/default/inverse-subtle. The +N bubble is surface/glass/white/md on the inverse card and surface/glass/midnight/lg on the default one.

On this page