figma guide
Designing product recommendations UI in Figma: rails, bundles, and handoff
Design you-may-also-like carousels, frequently bought together bundles, and personalized recommendation rails in Figma with component variants and Dev Mode specs.
- Published
- Updated
- Jun 23, 2026
- Read time
- 7 min
- Level
- Beginner
Quick answer
Recommendation UI is a reusable ProductRail with a typed header and a horizontal row of ProductCard instances—never one-off frames per page. Model rail types (related, fbt, recently_viewed, trending) as component properties so engineering can map slots to recommendation APIs. On PDP, stack Related items below reviews and Frequently bought together above the buy box only when the bundle adds a clear ATC shortcut. Reuse product cards and PDP variants; cap rails at 4–8 visible cards with scroll or peek. Pair with cart upsell drawers and empty states when a rail has no results. Start from the Figma guides hub.
Who this is for
- Product designers shipping DTC stores, marketplaces, and subscription catalogs where discovery drives average order value.
- Design system teams standardizing cross-sell rails across PDP, cart, checkout, and post-purchase surfaces.
- Engineers wiring recommendation engines, bundle pricing, carousel analytics, and lazy-loaded shelves.
Recommendation surfaces
| Surface | Goal | Key components |
|---|---|---|
| PDP — related | Continue browsing similar items | ProductRail type=related |
| PDP — FBT | Increase basket size | BundleRail with checkboxes |
| Cart drawer | Last-minute add-ons | ProductRail type=cart_upsell |
| Empty cart | Recovery merchandising | ProductRail type=trending |
| Order confirmation | Post-purchase cross-sell | ProductRail type=replenishment |
| Home / category hero | Merchandised shelves | ProductRail type=editorial |
| Search zero results | Alternative discovery | ProductRail type=popular |
Verdict: one ProductCard component everywhere—recommendation rails only change header copy, card count, and optional bundle chrome. Do not fork card layouts per rail type.
ProductRail anatomy
| Part | Purpose | Spec tip |
|---|---|---|
| Header | Context + optional link | ”You may also like” / “View all” |
| Subhead (optional) | Algorithm transparency | ”Based on items in your cart” |
| Scroll container | Horizontal shelf | Peek next card 16–24px |
ProductCard × n | Merchandise unit | Same as PLP grid |
| Nav arrows (desktop) | Carousel control | Disabled at start/end |
| Pagination dots (mobile) | Position indicator | Optional if swipe-only |
ProductRail
├── Variant: type=related | fbt | trending | recently_viewed | cart_upsell
├── Variant: density=compact | standard
├── Variant: breakpoint=desktop | mobile
├── Property: maxVisible (number)
├── Property: showViewAll (boolean)
└── Layers:
├── RailHeader (title + optional ViewAllLink)
├── RailSubhead (optional)
├── ScrollTrack
│ └── ProductCard × n
└── CarouselControls (arrows | dots)
Hide the entire rail when itemCount=0—do not render an empty carousel shell. Document fallback: show type=trending or omit section.
Frequently bought together (FBT)
FBT is not a standard product rail—it is a bundle selector with combined pricing:
| Part | Purpose | Spec tip |
|---|---|---|
| Primary product | Anchor SKU | Thumbnail + title (read-only) |
| Checkbox rows | Optional add-ons | Pre-checked defaults from merchandising |
| Plus separators | Visual grouping | ”+” between items |
| Bundle price | Sum with discount | Show savings vs individual |
| Primary CTA | Add selected to cart | ”Add 3 items to cart — $89” |
| Individual links | Escape hatch | ”View” per item → PDP |
BundleRail
├── Variant: layout=horizontal | stacked
├── Property: anchorProductId (string)
├── Property: selectedIds (string[])
└── Layers:
├── BundleItems (checkbox + ProductMini × n)
├── PriceSummary (total + savings)
└── AddBundleButton
When only one add-on exists, collapse to a simple “Complete the look” pair row—avoid a heavy bundle UI for two items.
Rail types and copy
| Type | Header example | When to show |
|---|---|---|
| related | ”You may also like” | Always on PDP if API returns ≥3 |
| fbt | ”Frequently bought together” | PDP when bundle rules exist |
| recently_viewed | ”Recently viewed” | Signed-in or cookie-backed |
| trending | ”Popular right now” | Fallback + homepage |
| cart_upsell | ”Customers also bought” | Cart with ≥1 line item |
| replenishment | ”Buy again” | Order history / confirmation |
| editorial | Campaign title | Merch-curated; optional hero link |
Keep headers honest—“Picked for you” implies personalization; “Similar items” is safer for rule-based related products.
ProductCard in rails vs PLP
| Attribute | PLP grid card | Rail card |
|---|---|---|
| Width | Fluid column | Fixed (160–220px) |
| Wishlist toggle | Yes | Optional (sm) |
| Quick add | Sometimes | Rare—tap opens PDP |
| Compare checkbox | Sometimes | Usually hidden |
| Rating | Full | Compact stars |
| Price | Full | Single line |
Use ProductCard variant context=rail so image ratio, title lines, and badge placement stay aligned with grid cards.
Cart and checkout upsell
| Placement | Pattern | Caution |
|---|---|---|
| Cart below line items | Single ProductRail | Max 6 cards; do not push totals below fold |
| Mini cart drawer | Compact rail | 3 cards + horizontal scroll |
| Checkout (pre-pay) | One-line add-on | Single SKU checkbox only—avoid carousel distraction |
| Post-ATC modal | Optional upsell | Dismissible; respect focus trap |
Checkout upsell should never block payment—use optional checkbox add-on, not a full carousel on the payment step.
Loading, empty, and error states
| State | Visual | Behavior |
|---|---|---|
| Loading | Skeleton cards in rail | Reserve rail height to prevent layout shift |
| Empty API | Hide rail entirely | Fallback to trending or nothing |
| Partial (1–2 items) | Show rail without carousel | Left-align cards; no arrows |
| Error | Omit rail | Silent fail—log server-side |
| Stale recently viewed | Gray “Unavailable” card | Remove slot or replace |
Document minimum item threshold (usually 3) before rendering a carousel—two cards look broken with arrow chrome.
Personalization and privacy
| Scenario | UX | Spec note |
|---|---|---|
| Signed-in personalization | Subhead: “Based on your browsing” | Link to privacy / ad prefs in footer |
| Guest cookie rail | Same copy without name | Cookie banner compliance (regional) |
| Sensitive categories | Suppress rails | Health, adult—engineering flag |
| Price in rail | Live price API | Stale price max age annotation |
Do not design recommendation UI that implies reading private data (“Because you bought X yesterday”) unless product/legal approves copy.
Accessibility
| Requirement | Implementation |
|---|---|
| Carousel | aria-roledescription="carousel" on track |
| Scroll | Keyboard arrows move focus between cards |
| Arrows | aria-label="Next products" / disabled state |
| Live region | Optional count: “Showing 4 of 12” |
| Motion | Respect prefers-reduced-motion—disable auto-play |
| Focus | Visible focus ring on card links |
Cross-check contrast on card scrims with accessibility plugins.
Handoff checklist
| Item | Dev Mode annotation |
|---|---|
| Rail type enum | Map to API slot names |
| Min/max items | Render thresholds |
| Card component | ProductCard variant=rail |
| Bundle pricing | Discount rule ID + currency |
| Lazy load | Intersection observer trigger |
| Analytics | select_item, view_item_list with item_list_name |
| Carousel swipe | Touch vs arrow-only on desktop |
| View all URL | Category or dedicated shelf page |
| Exclude OOS | Filter rule for recommendation SKUs |
Use Dev Mode checklist and reuse Auto Layout for scroll tracks.
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| Unique card per rail | Inconsistent PLP ↔ PDP | Single ProductCard variants |
| FBT without bundle CTA | Extra clicks | One “Add all” with selected checkboxes |
| Carousel with 2 items | Awkward arrows | Static row below threshold |
| Rails above buy box on mobile | Pushes ATC down | Related below fold; FBT only if high lift |
| Fake “personalized” copy | Trust issues | Match copy to actual algorithm |
| Infinite auto-play | Accessibility + annoyance | Manual scroll only |
| OOS items in rail | Dead-end taps | Filter unavailable SKUs |
Recommended workflow
- Extend
ProductCardwithcontext=railwidth and compact rating. - Build
ProductRailwith type property and header variants. - Add
BundleRailfor FBT with checkbox rows and bundle CTA. - Place rails on PDP (below reviews), cart, and empty cart.
- Design loading skeleton matching card dimensions.
- Document thresholds (min items, hide rules, fallbacks).
- Prototype carousel scroll and bundle checkbox → price update.
- Mobile pass with swipe, peek, and sticky buy box unaffected.
FAQ
Related vs cross-sell vs upsell?
Related = similar catalog items. Cross-sell = complementary category (case + screen protector). Upsell = higher-tier version of same product. Use distinct rail types so analytics and APIs stay separate.
How many rails on one PDP?
Cap at two merchandising blocks below the fold—related + FBT or related + reviews-adjacent shelf. More rails feel like ads and hurt conversion.
Sponsored products?
Add subtle “Sponsored” label on card or rail subhead; design isSponsored boolean on ProductCard. Do not mix sponsored and organic without disclosure.
Same rail in email?
Export card aspect ratio notes for ESP templates—design system should document 2:3 image ratio for rail cards reused in email grids.
Next steps
- Design product detail pages in Figma — where recommendation rails attach
- Design product listing grids in Figma — shared
ProductCardsource - Design shopping cart UI in Figma — cart upsell placement
- Design cards in Figma — rail card layout variants
- Design loading states in Figma — skeleton shelves
§ Keep reading