figma guide
Designing coupons and promo codes UI in Figma: discounts, validation, and handoff
Design promo code inputs, applied discounts, coupon cards, and cart totals in Figma with validation states, stacking rules, and Dev Mode specs for ecommerce checkout.
- Published
- Updated
- Jun 20, 2026
- Read time
- 8 min
- Level
- Beginner
Quick answer
Promo code UI spans an entry field (cart or checkout), validation feedback, an applied-coupon row in OrderSummary, and optional coupon cards on marketing surfaces—each state (empty, typing, loading, success, error, expired) must be explicit. Build PromoCodeField with input + Apply button, AppliedCoupon with code label and remove action, and DiscountRow in the totals block using price display tokens. Document stacking rules (one code vs multiple), auto-apply from URL, and free-shipping vs percentage vs fixed-amount discounts. Pair with cart/checkout, forms, and inline alerts. Start from the Figma guides hub.
Who this is for
- Product designers shipping DTC stores, marketplaces, and subscription checkout with promotional pricing.
- Design system teams aligning discount display across cart, checkout, PDP, and account order history.
- Engineers implementing promo validation APIs, URL deep links (
?promo=SAVE20), and tax recalculation after discounts.
Promo surfaces
| Surface | Goal | Key components |
|---|---|---|
| Cart promo field | Apply before checkout | PromoCodeField, AppliedCoupon |
| Checkout promo field | Last-chance apply | Same components—do not redesign |
| OrderSummary discount rows | Show savings | DiscountRow, strikethrough subtotal |
| PDP / PLP sale badge | Surface active promos | Badge + optional code reveal |
| Coupon wallet (account) | Saved offers | CouponCard list |
| Marketing landing | Copy code CTA | CouponCard hero variant |
Verdict: one PromoCodeField + AppliedCoupon pair reused in cart and checkout—different copy is fine; different layouts create support tickets when totals look different between steps.
PromoCodeField anatomy
| Part | Purpose | Spec tip |
|---|---|---|
| Label | ”Promo code” or “Discount code” | Optional on compact cart—use placeholder |
| Text input | Code entry | Uppercase transform optional; trim whitespace |
| Apply button | Submit code | Disabled when empty; loading state during API |
| Helper text | Format hint | ”Enter code without spaces” |
| Error message | Validation failure | Below field, text/error token |
| Success message | Applied confirmation | Brief—row in summary is primary feedback |
PromoCodeField
├── Variant: layout=inline | stacked
├── Variant: state=empty | focus | loading | error | success | disabled
├── Property: code (string)
└── Layers:
├── Label (optional)
├── InputRow (input + Apply)
├── HelperText
└── ErrorMessage / SuccessMessage
Use form input states for focus rings and error borders. On mobile, prefer stacked layout (full-width input, full-width Apply below) to avoid cramped inline fields.
Validation states
| State | Trigger | Visual | Copy examples |
|---|---|---|---|
| Empty | Default | Neutral border | Placeholder: “Enter code” |
| Focus | User taps field | Focus ring | — |
| Loading | Apply clicked | Spinner on button; input disabled | — |
| Invalid | API 400 | Red border + message | ”This code isn’t valid” |
| Expired | Date passed | Red border | ”This offer expired on Jun 1” |
| Not eligible | Cart rules fail | Amber alert | ”Add $25 more to use SAVE20” |
| Already applied | Duplicate submit | Info message | ”SAVE20 is already applied” |
| Success | Valid code | Green check optional; show AppliedCoupon | ”20% off applied” |
Prototype Apply → loading → success/error with interactive components swapping state property. Production should debounce double-clicks and disable Apply during loading.
AppliedCoupon component
| Part | Purpose | Spec tip |
|---|---|---|
| Code label | What was applied | Monospace optional: SAVE20 |
| Discount description | Human-readable | ”20% off entire order” |
| Savings amount | Optional inline | ”−$12.40” in price sm |
| Remove action | Unapply code | ”Remove” link or × icon |
| Icon | Tag or ticket | 16–20px, icon/secondary |
AppliedCoupon
├── Variant: density=compact | comfortable
├── Property: code (string)
├── Property: discountType (percent | fixed | shipping)
└── Layers:
├── Icon
├── CodeLabel + Description
├── SavingsAmount (optional)
└── RemoveButton
Place above discount rows in OrderSummary or directly below the promo field—pick one pattern and document it. Removing a coupon should restore previous totals; show loading skeleton on totals during recalculation.
OrderSummary discount rows
| Row | When shown | Display |
|---|---|---|
| Subtotal | Always | Pre-discount item total |
| Item-level discounts | Per-line promos | ”Item discount” −$X |
| Order-level discount | Code applied | ”Promo SAVE20” −$X |
| Free shipping | Shipping promo | ”Free shipping” −$X or strikethrough shipping |
| Total savings (optional) | Marketing emphasis | ”You save $24.00” in text/success |
| Tax | After discounts | Note: tax base may change |
| Total | Final | Bold price lg |
| Discount type | Summary label | PDP/PLP signal |
|---|---|---|
| Percentage | ”20% off” | Sale badge on cards |
| Fixed amount | ”$10 off orders $50+“ | Threshold copy in alert |
| Free shipping | ”Free shipping” | Truck icon + badge |
| BOGO | ”Buy 1 get 1” | Line-item split in cart |
Show strikethrough on subtotal only when a visible discount applies—avoid fake “was” pricing without a real reference price (regulatory risk in some markets).
CouponCard (wallet and marketing)
| Part | Purpose | Spec tip |
|---|---|---|
| Headline | Offer value | ”20% off sitewide” |
| Code | Copy target | SAVE20 + copy button |
| Expiry | Urgency | ”Expires Jun 30” |
| Eligibility | Rules | ”Orders $50+ · Excludes sale items” |
| CTA | Apply or shop | ”Apply” auto-fills cart field |
| State | Active / used / expired | Gray out expired; checkmark on used |
CouponCard
├── Variant: context=wallet | marketing | inline
├── Variant: state=active | applied | expired | used
└── Layers:
├── ValueHeadline
├── CodeRow (code + CopyButton)
├── MetaRow (expiry + rules)
└── CTA
Copy button triggers toast “Code copied.” Deep link ?promo=SAVE20 should auto-apply on cart load—note in Dev Mode spec, not only in marketing copy.
Stacking and business rules
Document these in a spec table—design and engineering must match:
| Rule | UI impact |
|---|---|
| One code per order | Hide promo field after apply; Remove before new code |
| Stackable codes | Multiple AppliedCoupon rows; max count message |
| Auto-apply best deal | No field needed—show banner “Best offer applied” |
| Employee / loyalty codes | Separate field or account-only wallet |
| Subscription first-order | Different copy on checkout step 1 |
| Incompatible with gift card | Error: “Can’t combine with gift card balance” |
| Scenario | Recommended UX |
|---|---|
| Code works but $0 discount | ”Code applied—no additional savings on this order” |
| Better code available | Suggest swap: “Use SAVE25 instead? [Switch]“ |
| Price changed after apply | Alert + recalculate totals |
Responsive behavior
| Breakpoint | Promo field | Applied coupon |
|---|---|---|
| Mobile | Stacked input + button | Full width below items |
| Tablet | Inline or stacked | Same as mobile in drawer cart |
| Desktop | Inline in cart sidebar | Above OrderSummary rows |
In cart drawer, collapse promo behind “Have a promo code?” disclosure to reduce noise—expand on tap with focus on input.
Handoff checklist
| Item | Dev Mode annotation |
|---|---|
| Validation API response codes | Map to error copy table |
| Stacking policy | Max codes, exclusion list |
| URL auto-apply param | ?promo= behavior |
| Tax recalculation | Order of discount → tax |
| Remove coupon endpoint | Optimistic UI + rollback |
| Copy-to-clipboard | CouponCard + field paste |
| Analytics | apply_promo, remove_promo, promo_error |
| Order history | Show applied code on order detail |
Use Dev Mode checklist and share DiscountRow props with checkout OrderSummary.
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| Different promo UI in cart vs checkout | User confusion | Single component set |
| No loading state on Apply | Double-submit bugs | Disable input + button |
| Error only in toast | Missed on mobile | Inline error under field |
| Discount not reflected in line items | ”Where did savings go?” | Show per-line or summary row |
| Expired code with generic error | Support volume | Specific expired copy + date |
| Hiding remove action | Stuck wrong code | Always show Remove on AppliedCoupon |
Recommended workflow
- Audit discount types your store supports (%, fixed, shipping, BOGO).
- Build
PromoCodeFieldwith full validation state matrix. - Add
AppliedCouponandDiscountRowto existing cartOrderSummary. - Create
CouponCardfor wallet and campaign landing mocks. - Document stacking rules on a spec page linked from components.
- Prototype apply → error → remove → reapply flow.
- Align order history to show code + savings on past orders.
- Run a11y check on error announcements and copy buttons.
FAQ
Collapsible “Have a promo code?” or always visible?
Collapsible on mini-cart and mobile checkout reduces clutter; always visible on full cart page where users expect it. Use the same expanded component in both.
Show savings on product cards before cart?
Show sale price and badge when a catalog-wide promo is active; hide unique one-time codes until cart unless marketing requires teaser copy.
Gift cards vs promo codes?
Separate UI—gift card balance is a payment method, not a discount row. If mutually exclusive, say so in error copy.
Auto-apply from email links?
Design silent apply with success banner in cart; fallback to manual entry if session expired.
Next steps
- Design shopping cart and checkout UI in Figma — OrderSummary and promo placement
- Design price tags and pricing UI in Figma — strikethrough and savings display
- Design forms in Figma — input validation patterns
- Design inline alerts and banners in Figma — eligibility and threshold messages
§ Keep reading