Figma 购物车与滑出式结算抽屉系统(Shopping Cart & Slideout Checkout Drawer Architecture)组件架构实战:从商品规格 SKU 变体选择、数量步进器、满减优惠/运费计算明细、固定底部结算栏到 Variables 价格联动与 Dev Mode 现代电商交付

2026 年 11 月 14 日

一、电商转化漏斗中的购物车抽屉(Mini-Cart Drawer)交互模型 #

在跨境电商独立站(Shopify/WooCommerce)、品牌 DTC 官网以及综合消费类移动应用中,购物车抽屉(Slideout Cart Drawer / Mini-Cart) 是连接浏览导购页面与最终结账支付的核心转化枢纽。

传统电商往往直接将用户粗暴地从商品详情页重定向至独立的 /cart 全页面,这种设计破坏了用户的连续选购心流,不仅大幅增加了页面跳出率,还使跨页面再次加购的链路变得异常繁琐。现代电商普遍采用**右侧滑出式抽屉(Slideout Drawer)或移动端底部抽屉(Bottom Sheet)**形态,使用户在保持当前浏览视口上下文的同时,能够即时预览所购商品明细、动态微调 SKU 规格尺寸、实时查看包邮进度门槛,并一键直达支付结账。

在 Figma 中构建高质量的购物车抽屉组件系统时,核心挑战往往集中在以下方面:

  1. 纵向流式容器与吸底结算栏的冲突:如何确保商品清单在不同屏幕高度下自由滚动,而底部的结账汇总面板(Sticky Footer Summary)稳固吸底且不遮挡最后一个商品。
  2. 多态商品卡片拓扑:商品可能处于正常态、加购成功高亮态、缺货失效态(Out of Stock)、达到限购上限态或附带促销赠品态。
  3. 动态价格与免运费阶梯的逻辑联动:加减商品数量时,小计金额、满额免运费进度条以及整单总价如何通过 Variables 表达式实现即时动态联动。
  4. 多端响应式工程交付:桌面端表现为右侧滑出面板(宽度 400px~480px),移动端自适应切换为全宽吸底滑块(Bottom Sheet),并与现代前端(React / Tailwind / Shopify Liquid)精准对齐。

本文将深入拆解购物车与滑出式结算抽屉系统的组件架构与工程实现。


二、购物车列表商品条目(Cart Item)原子组件架构 #

购物车商品条目(Cart Item)是整个抽屉中复用频次最高的原子组件。如果结构组织混乱,当商品信息过长或包含多重促销标签时极易出现换行错位。

1. 商品条目 Auto Layout 容器层级拆解 #

Cart Item Container (Horizontal Auto Layout: Fill, Padding: 16px 0, Border Bottom)
 ├── Checkbox Selection (Component Instance, Boolean: selectable, Align: Center)
 ├── Product Thumbnail (Frame: 80px * 80px / 80px * 106px, Corner: 8px, Clip)
 │    ├── Product Image (Fill Container, Mode: Cover)
 │    └── Badge Tag: Discount / Out of Stock (Absolute / Top-Left)
 └── Info & Controls Column (Vertical Auto Layout: Fill, Spacing: 8px)
      ├── Header Row (Horizontal Auto Layout: Fill, Space Between)
      │    ├── Product Title (Text: 14px SemiBold, Max 2 lines Truncate)
      │    └── Remove Button (Icon Instance / "trash-2", 16x16, Hover State)
      ├── Variant Specs Row (Horizontal Auto Layout: Hug, Spacing: 4px)
      │    └── SKU Pill Button (Frame / Hug: "黑色 / XL ▼", Interactive Dropdown)
      └── Price & Quantity Stepper Row (Horizontal Auto Layout: Fill, Space Between)
           ├── Quantity Stepper Component (Horizontal Auto Layout, Border, Corner: 6px)
           │    ├── Minus Button (Button, Disabled when Qty=1)
           │    ├── Qty Display (Text: "2", Width: 28px, Align: Center)
           │    └── Plus Button (Button, Disabled when Max reached)
           └── Price Group (Vertical / Horizontal: Align Right)
                ├── Original Line-through Price (Text: "¥299", text-muted)
                └── Current Final Price (Text: "¥199", text-brand, 16px Bold)

2. 核心状态与变体(Variants)设计 #

  • Stock Status(Variant):
    • In Stock(正常现货):操作按钮全可用。
    • Low Stock(库存紧张):显示红字提醒「仅剩 2 件,抢购中」。
    • Out of Stock(已售罄):整行透明度降为 50%,步进器禁用,右侧替换为「失效」提示。
  • SKU Editable(Boolean):控制是否支持在抽屉内直接点击弹出轻量规格下拉框进行尺码/颜色二次修改,避免用户退回详情页。
  • Promo Tag(Boolean):控制是否展示「满减折上折」或「限时买赠」徽标。

三、滑出式抽屉全容器 Auto Layout 拓扑编排 #

滑出式抽屉的骨架结构决定了最终页面的伸缩弹性与开发交付的纯净度。

1. 抽屉骨架的纵向弹性布局 #

抽屉整体为一个竖向 Auto Layout 容器(宽度定为 440px,高度设为 Fill Container 或定高 100vh),在内部从上至下划分为四大独立区块:

  1. 抽屉顶部标题栏(Drawer Header,Hug Height):
    • 水平排列:左侧标题「我的购物车 (3)」,中间展示全选复选框,右侧为关闭「✕」图标按钮。
    • 底部附加浅灰色分割线(border-subtle,1px)。
  2. 动态包邮激励进度条(Free Shipping Motivator,Hug Height):
    • 容器背景采用浅品牌色柔和渐变或微弱阴影(surface-brand-subtle)。
    • 顶部文字:「还差 ¥38.00 即可享受全场免费冷链配送!」。
    • 下方放置动态进度条(Progress Bar):外层轨道高度 6px,圆角 999px;内部高亮填充条(Fill Bar)通过变量联动其宽度百分比。
  3. 滚动商品列表视口(Scrollable Items Viewport,Fill Container):
    • 关键设置:垂直方向必须设置为 Fill Container(填充容器),且在原型设置中开启 Vertical Scrolling(纵向滚动)。
    • 内部纵向排列商品条目实例,设置合适的垂直间距(16px)与内边距(Padding: 0 20px)。
  4. 固定底部结算面板(Sticky Footer Checkout Bar,Hug Height):
    • 无论中间商品列表有多少件,底部结算面板始终牢牢吸附在抽屉底部,绝不随滚动条向上跑动。
    • 包含:促销优惠码折叠输入框、金额小计(Subtotal)、折扣抵扣(Discount)、预估运费(Shipping)、合计金额(Total)、一键结账 CTA 主按钮(Checkout Button)以及多币种支付安全合规徽标(Visa / MasterCard / Apple Pay 等)。

四、技术方案对比:全页固定模态 vs 右侧滑出抽屉 vs 悬浮气泡浮层 #

在电商转化链路中,设计团队常在以下三种载体之间进行权衡取舍:

评估维度 全页购物车页面(Full Cart Page) 右侧滑出抽屉(Slideout Drawer,推荐) 悬浮下拉气泡(Mini Cart Popover)
首屏选购连续性 差(强制跳转新 URL,离开当前页面) 极佳(页面半透明遮罩,保留当前商品背景) 良好(局部悬浮气泡展开)
信息承载能力 极强(适合展示复杂的跨品类换购) 强(纵向滚动流畅展示 10+ 商品及促销进度) 弱(空间极其狭窄,极易产生溢出截断)
移动端适配一致性 需针对移动端彻底重构排版 极佳(直接平滑过渡为底部抽屉 Bottom Sheet) 极差(移动端几乎无法良好悬停)
平均结账转化率 (CVR) 基准线 相比全页跳转平均提升 12%~25% 的直接结账率 仅适合 1~2 件小物件冲动消费场景
前端实现复杂度 中等(标准页面路由) 适中(标准 Offcanvas / Dialog 组件) 复杂(需处理悬停防抖与视口边缘碰撞)

从现代移动端优先(Mobile-First)与全球电商转化最佳实践来看,**右侧滑出抽屉(结合移动端底部抽屉)**是当前全行业公认的最优解。


五、Variables 变量与计算表达式在价格联动中的高阶实战 #

Figma 的 Variables 变量系统不仅支持颜色与文本切换,还支持数字(Number)类型之间的四则运算与条件表达式(Expressions)。利用这一特性,可以在设计评审与用户测试中展现一个完全能够实时计算价格的真实验算原型。

1. 变量字典(Variable Collection: "Ecommerce-Cart") #

建立如下全局变量表:

变量名 (Variable Name) 类型 (Type) 初始值 (Default Value) 说明与逻辑公式
item_count Number 2 购物车中商品总件数
unit_price_item1 Number 120 商品 1 的单价
qty_item1 Number 1 商品 1 的加购数量
unit_price_item2 Number 80 商品 2 的单价
qty_item2 Number 1 商品 2 的加购数量
subtotal_amount Number 200 小计金额:unit_price_item1 * qty_item1 + ...
free_shipping_threshold Number 250 满额免运费门槛线(如满 250 包邮)
shipping_fee Number 15 当 subtotal >= 250 时降为 0,否则为 15
final_total Number 215 subtotal_amount + shipping_fee

2. 数量步进器(Stepper)加减计算事件连线 #

在商品 1 的数量步进器「+」按钮上绑定交互事件:

  1. Trigger:On click。
  2. Action 1:Set variable -> qty_item1 = qty_item1 + 1。
  3. Action 2:Set variable -> subtotal_amount = subtotal_amount + unit_price_item1。
  4. Action 3(条件判断 Conditional):
    • if subtotal_amount >= free_shipping_threshold
    • Set variable -> shipping_fee = 0
    • else
    • Set variable -> shipping_fee = 15
  5. Action 4:Set variable -> final_total = subtotal_amount + shipping_fee。

通过上述变量逻辑链,测试人员在点击「+」增加商品数量时,抽屉中的小计、运费变化、包邮状态提示词以及底部结账按钮上的总金额将同步刷新,带来无与伦比的原型交互体验。


六、Dev Mode 现代前端电商工程化交付(Shopify Hydrogen & React) #

在交付给前端团队(尤其是基于 React、Next.js 或 Shopify Hydrogen 架构的工程团队)时,需在 Dev Mode 中清晰注明抽屉的语义化 DOM 结构、状态管理边界与 WAI-ARIA 无障碍要求。

1. 无障碍规范对照清单 #

  • 抽屉外层容器:赋予 role="dialog" 与 aria-modal="true",并指定 aria-labelledby="cart-drawer-title",确保屏幕阅读器能正确感知弹窗上下文。
  • 背景遮罩层(Backdrop):点击遮罩必须触发关闭回调,且当抽屉展开时,页面主体的其他元素需附加 aria-hidden="true" 防止焦点穿透。
  • 金额更新区域:在小计与合计金额包裹层声明 aria-live="polite",当数量增减导致总价变动时,无障碍辅助设备能自动平缓播报最新的金额,无需刷新页面。

2. React + Tailwind CSS 生产级组件交付范例 #

import React from 'react';
import { X, Trash2, Plus, Minus, Lock } from 'lucide-react';

interface CartItem {
  id: string;
  title: string;
  variant: string;
  price: number;
  quantity: number;
  image: string;
}

interface CartDrawerProps {
  isOpen: boolean;
  onClose: () => void;
  items: CartItem[];
  onUpdateQty: (id: string, delta: number) => void;
  onRemove: (id: string) => void;
}

export const SlideoutCartDrawer: React.FC<CartDrawerProps> = ({
  isOpen,
  onClose,
  items,
  onUpdateQty,
  onRemove,
}) => {
  if (!isOpen) return null;

  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const freeShippingGoal = 250;
  const progressPercent = Math.min(100, (subtotal / freeShippingGoal) * 100);
  const shippingFee = subtotal >= freeShippingGoal ? 0 : 15;
  const total = subtotal + shippingFee;

  return (
    <div className="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true">
      {/* 半透明背景遮罩 */}
      <div 
        className="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" 
        onClick={onClose} 
      />

      {/* 滑出抽屉主体容器 */}
      <div className="relative flex flex-col h-full w-full max-w-md bg-white shadow-2xl dark:bg-slate-900 z-10 transition-transform">
        
        {/* 顶部标题栏 */}
        <div className="flex items-center justify-between px-6 py-4 border-b border-slate-100 dark:border-slate-800">
          <h2 id="cart-drawer-title" className="text-lg font-bold text-slate-900 dark:text-slate-100">
            我的购物车 ({items.reduce((acc, cur) => acc + cur.quantity, 0)})
          </h2>
          <button onClick={onClose} className="p-1 rounded-full text-slate-400 hover:text-slate-600">
            <X className="w-5 h-5" />
          </button>
        </div>

        {/* 免运费进度条 */}
        <div className="px-6 py-3 bg-blue-50/60 dark:bg-blue-950/30">
          <p className="text-xs font-medium text-blue-900 dark:text-blue-200 mb-1.5">
            {subtotal >= freeShippingGoal
              ? '🎉 恭喜!已解锁全场免费极速配送'
              : `再凑 ¥${(freeShippingGoal - subtotal).toFixed(2)} 即可享受免运费`}
          </p>
          <div className="h-1.5 w-full bg-blue-100 rounded-full overflow-hidden dark:bg-blue-900">
            <div 
              className="h-full bg-blue-600 rounded-full transition-all duration-300"
              style={{ width: `${progressPercent}%` }}
            />
          </div>
        </div>

        {/* 可滚动商品列表区 */}
        <div className="flex-1 overflow-y-auto px-6 py-4 divide-y divide-slate-100 dark:divide-slate-800">
          {items.map((item) => (
            <div key={item.id} className="flex gap-4 py-4">
              <img src={item.image} alt={item.title} className="w-20 h-24 object-cover rounded-md bg-slate-100" />
              <div className="flex-1 flex flex-col justify-between">
                <div>
                  <div className="flex justify-between items-start">
                    <h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200 line-clamp-1">{item.title}</h3>
                    <button onClick={() => onRemove(item.id)} className="text-slate-400 hover:text-red-500">
                      <Trash2 className="w-4 h-4" />
                    </button>
                  </div>
                  <span className="inline-block mt-1 text-xs text-slate-500 bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded">
                    {item.variant}
                  </span>
                </div>
                <div className="flex justify-between items-center mt-3">
                  <div className="flex items-center border border-slate-200 dark:border-slate-700 rounded-md">
                    <button 
                      onClick={() => onUpdateQty(item.id, -1)} 
                      disabled={item.quantity <= 1}
                      className="px-2 py-1 text-slate-500 disabled:opacity-40"
                    >
                      <Minus className="w-3.5 h-3.5" />
                    </button>
                    <span className="px-2 text-xs font-semibold">{item.quantity}</span>
                    <button onClick={() => onUpdateQty(item.id, 1)} className="px-2 py-1 text-slate-500">
                      <Plus className="w-3.5 h-3.5" />
                    </button>
                  </div>
                  <span className="text-sm font-bold text-slate-900 dark:text-slate-100">
                    ¥{(item.price * item.quantity).toFixed(2)}
                  </span>
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* 吸底结算固定面板 */}
        <div className="p-6 border-t border-slate-100 bg-slate-50/50 dark:border-slate-800 dark:bg-slate-900/80">
          <div className="space-y-2 text-sm text-slate-600 dark:text-slate-400">
            <div className="flex justify-between">
              <span>商品小计</span>
              <span className="font-semibold text-slate-900 dark:text-slate-100">¥{subtotal.toFixed(2)}</span>
            </div>
            <div className="flex justify-between">
              <span>预估运费</span>
              <span>{shippingFee === 0 ? '免费' : `¥${shippingFee.toFixed(2)}`}</span>
            </div>
            <div className="flex justify-between text-base font-bold text-slate-900 dark:text-slate-100 pt-2 border-t border-slate-200 dark:border-slate-700" aria-live="polite">
              <span>应付总额</span>
              <span className="text-blue-600 dark:text-blue-400">¥{total.toFixed(2)}</span>
            </div>
          </div>
          <button className="w-full mt-4 flex items-center justify-center gap-2 py-3.5 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg shadow-blue-500/20 transition-all">
            <Lock className="w-4 h-4" />
            安全结算(立即支付)
          </button>
        </div>

      </div>
    </div>
  );
};

七、站内相关进阶学习资源 #

构建完高保真购物车与结算抽屉后,推荐继续研读站内以下实战指南以完善整套电商交互闭环:

❓ 常见问题与答疑 (FAQ)

高频疑难解析与避坑指南
A

完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。