Figma 多维分面筛选与侧边属性过滤面板系统(Faceted Filter & Multi-Criteria Refinement Panel Architecture)组件架构实战:从抽屉/侧栏双模式视口、属性分组手风琴(Facet Group Accordion)、已选胶囊徽标联动(Filter Chips)到 Variables 状态机与 Dev Mode 现代前端交付
引言:复杂数据检索的体验枢纽——分面筛选(Faceted Filter) #
在现代电商商城、B2B SaaS 资产库、云平台资源管理器以及大型知识库中,用户极少只依赖单一关键字搜索。面对成千上万条海量数据,用户更依赖 分面筛选(Faceted Search & Filtering),通过品牌、价格区间、评分、规格、状态与创建时间等多维属性交叉下钻,快速锁定目标。
然而,分面筛选面板往往是界面设计与工程还原中复杂度极高的交互组件:
- 跨视口形态割裂:桌面端常作为左侧固定或吸顶的列式面板,移动端则演变为底部滑出抽屉或全屏模态框,两者组件资产若未同构解耦,会导致多套维护灾难。
- 多层折叠高度冲突:每个属性维度通常采用手风琴(Accordion)折叠结构,当展开多个维度时,内容溢出视口导致滚动穿透或与主页面滚动条打架。
- 已选条件管理混乱:多选属性后,已选标签(Filter Chips)无法动态折行排版,或者缺少一键清除(Clear All)与单项剔除逻辑,用户无法直观获知当前筛选状态。
- 前端状态同步脱节:设计稿仅呈现静态面板,未考虑筛选条件与浏览器地址栏 URL 参数(Query String)的解耦映射,导致用户无法复制带有筛选结果的链接。
本文将系统拆解如何利用 Figma 最新特性,从桌面侧栏/移动抽屉双模适配、属性分组手风琴插槽、双滑块区间选择器、已选条件胶囊折行、Variables 计数状态机到 Dev Mode 现代前端代码,构建生产级分面筛选系统。
分面筛选容器视口拓扑:桌面侧栏 vs 移动端抽屉 #
在构建组件前,首先需理清桌面端与移动端的交互拓扑差异:
| 交互维度 | 桌面端固定侧边栏 (Desktop Sticky Sidebar) | 移动端滑出抽屉 (Mobile Slideout Drawer) |
|---|---|---|
| 容器形态 | 宽度固定(260px ~ 320px),高度 Fill 随页面吸顶滚动 | 覆盖视口底边或侧边(宽度 85vw 或全屏抽屉),带暗色遮罩 |
| 触发与展示时机 | 大屏常驻展开,或支持点击收起为窄图标栏(Collapsible) | 默认隐藏,通过顶部的“筛选 (Filters) + 徽标计数”按钮呼出 |
| 生效机制 | 即时响应 (Instant Apply):每次勾选复选框,右侧列表即刻重绘 | 批处理应用 (Batch Apply):抽屉底部固定“重置”与“查看 N 项结果”CTA |
| 已选条件展示 | 在筛选栏顶部或列表顶栏平铺展示已选胶囊(Filter Chips) | 在抽屉内展示小红点计数,确认后回填到主界面横向可滑动条 |
核心实操一:属性分组手风琴插槽(Facet Group Accordion) #
分面面板的骨架是由多个可折叠的属性组(Facet Groups)堆叠而成。必须将其封装为高度模块化的手风琴组件。
1. 手风琴标头(Accordion Header)结构 #
- 创建横向 Auto Layout,Padding: 上下
12px、左右0,宽度设为Fill container。 - 左侧放置标题文字(如“价格区间”、“适用平台”),字号推荐
14px / 600 SemiBold。 - 中间插入弹性填充空间(Spacer 图层或 Auto Layout 间距设为
Auto)。 - 右侧组合插槽:
- 已选数量徽标(Badge):仅在当前分组被选中时显示(如
· 2),背景使用浅灰或品牌浅色; - 旋转箭头指示器(Chevron Icon):展开态旋转
180°,收起态为0°。
- 已选数量徽标(Badge):仅在当前分组被选中时显示(如
2. 组内多态选项容器(Facet Content Slot) #
在展开的内容区内,针对不同数据类型配置多态原子组件:
- 复选框列表(Checkbox List):适用于品牌、分类等离散属性。每个项目采用 Auto Layout 横向排布,右侧附带符合该条件的匹配结果数(如
Apple (128))。 - 单选分段控制器(Segmented Control):适用于互斥属性(如“全新 / 二手 / 官翻”)。
- 色块矩阵(Color Swatches):适用于服饰或视觉类电商,采用 Auto Layout Wrap 自动折行,呈现圆角方形色块并带有选中白色对勾与高亮外边框。
- 组内即时搜索框(In-facet Search Input):针对品牌或型号超过 10 项的大分组,在顶部嵌入高度
32px的微型搜索框,支持组内二次模糊过滤。
3. “展开更多/收起(Show More / Less)”渐进展开 #
当选项超过 5 个时,默认只显示前 5 项,底部提供浅色文字按钮“展开更多 (+12)”。在 Figma 中通过切换 Boolean Property 属性显隐其余列表项,保持初始视口的整洁清爽。
核心实操二:数值/价格区间双滑块与数字输入框对齐(Dual-Handle Range Slider) #
数值筛选(如价格、评分、里程数)需要支持连续区间的拖拽与离散数值的手动录入。
1. 搭建双滑块轨道几何模型 #
- 背景灰色轨道(Inactive Track):创建矩形 Frame,高度
4px,宽度Fill container,圆角2px,填充中性灰#E5E7EB。 - 激活高亮轨道(Active Range Bar):在背景轨道上方放置主题色矩形(
#3B82F6),开启绝对定位,左右边距由滑动区间动态决定。 - 左右拖拽手柄(Min & Max Handles):
- 制作两个
20px × 20px的正圆形白底滑块,带1px浅灰描边与立体阴影(Y: 2px, Blur: 4px, Color: rgba(0,0,0,0.1))。 - 在手柄上方可配置悬浮数值气泡(Tooltip),实时显示当前浮点数或金额。
- 制作两个
2. 下方输入框精细对齐(Min / Max Inputs) #
- 在滑块轨道下方
12px处放置横向 Auto Layout 容器。 - 包含:左侧最小值输入框(
Fill container)、中间连接破折号(—,宽度固定)、右侧最大值输入框(Fill container)。 - 输入框高度设为
32px,内嵌货币符号前缀(如¥)与数字文本,保证键盘直接输入与滑块手柄双向联动。
核心实操三:已选条件标签胶囊与全局清空联动(Active Filter Chips & Clear All) #
已选条件的显性化展示是分面筛选闭环的关键。
1. 已选胶囊(Filter Chip)原子规范 #
- 创建横向 Auto Layout,Padding: 上下
4px、左右8px,间距 Gap:4px,圆角设为6px或999px胶囊状。 - 填充色设为极浅品牌灰或浅蓝(如
#EFF6FF),描边设为1px(#DBEAFE)。 - 文本采用双段文字组合:属性类别前缀(
品牌:中性灰)+ 具体选定值(Nike黑色粗体)。 - 尾部放置微型移除叉号图标(
12px × 12px),悬停时颜色加深,提供直接剔除操作。
2. Auto Layout Wrap 自动折行容器 #
- 将所有已选 Chip 组合放入外层容器,在 Figma 右侧属性面板中开启 Auto Layout Wrap(自动折行)。
- 水平与垂直间距均设为
8px,容器宽度设为Fill container。 - 无论用户连续筛选了 3 项还是 15 项,标签都会自适应向下优雅换行,绝不产生非预期的横向截断或水平滚动条!
3. 全局“全部清除(Clear All)”操作联动 #
在 Chip 区域的右上角或筛选栏底部常驻放置“全部清除”文字按钮。
- 无选中状态:文字呈浅灰色(
text-zinc-400),禁用指针,不可点击; - 有选中状态:激活为高对比度主题色或危险红色(
text-blue-600/text-rose-600),点击后触发全局重置交互。
核心实操四:Variables 变量驱动的筛选状态机与计数徽标联动 #
通过 Figma 的 Local Variables,赋予静态画板准代码级的动态交互能力:
1. 核心状态变量配置表 #
| 变量名称 (Variable) | 变量类型 | 初始值 | 作用范围与驱动目标 |
|---|---|---|---|
selectedFiltersCount | Number | 0 | 主界面“筛选”按钮上的小红点数字徽标、底部“查看结果”按钮文案 |
isFilterDrawerOpen | Boolean | false | 控制移动端侧边抽屉与遮罩层的可见性(Layer: Visible) |
hasActiveFilters | Boolean | false | 控制“全部清除(Clear All)”按钮的显隐与禁用状态 |
priceRangeMax | Number | 5000 | 控制价格滑动条右侧手柄位置与输入框回填数值 |
2. 状态机流转与原型交互连线 #
- 展开移动端抽屉:
在顶部筛选按钮上添加交互动作:
On click->Set variable: isFilterDrawerOpen = true,并触发带有弹性遮罩的 Smart Animate 侧滑。 - 多选项目勾选:
在复选框项目上配置:
On click->Set variable: selectedFiltersCount = selectedFiltersCount + 1,同时触发hasActiveFilters = true。 - 全部清除:
在 Clear All 按钮上配置:
On click-> 将所有计数归零并隐藏已选胶囊容器。
核心实操五:Dev Mode 现代前端代码交付与 URL 参数解耦 #
在前端工程化落地中,分面筛选必须与 URL Query Parameters 深度绑定,确保用户可以随时刷新、后退前进或分享当前筛选页面。
1. 生产级 React + TypeScript + Tailwind 规范实现代码 #
在 Dev Mode 中,前端团队可直接参考以下兼顾 URL 同步、可折叠手风琴与无障碍交互的实现:
import React, { useState, useTransition } from 'react';
import { ChevronDown, X, SlidersHorizontal, RotateCcw } from 'lucide-react';
interface FacetOption {
id: string;
label: string;
count: number;
}
interface FacetSection {
id: string;
title: string;
options: FacetOption[];
}
const mockFacets: FacetSection[] = [
{
id: 'brand',
title: '品牌厂商',
options: [
{ id: 'apple', label: 'Apple', count: 142 },
{ id: 'sony', label: 'Sony', count: 89 },
{ id: 'bose', label: 'Bose', count: 45 },
],
},
{
id: 'connectivity',
title: '连接协议',
options: [
{ id: 'wireless', label: '无线蓝牙 5.3', count: 210 },
{ id: 'type-c', label: '有线 Type-C', count: 68 },
],
},
];
export const FacetedFilterSidebar: React.FC = () => {
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
const [openSections, setOpenSections] = useState<Record<string, boolean>>({ brand: true, connectivity: true });
const [isPending, startTransition] = useTransition();
// 切换分类手风琴展开态
const toggleSection = (sectionId: string) => {
setOpenSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
};
// 勾选/反选单项
const handleToggleOption = (sectionId: string, optionId: string) => {
startTransition(() => {
setSelectedFilters(prev => {
const current = prev[sectionId] || [];
const next = current.includes(optionId)
? current.filter(item => item !== optionId)
: [...current, optionId];
return { ...prev, [sectionId]: next };
});
// 实际业务中在此处调用 router.push 更新 URL searchParams
});
};
// 移除单个 Tag
const handleRemoveFilter = (sectionId: string, optionId: string) => {
handleToggleOption(sectionId, optionId);
};
// 全部清除
const handleClearAll = () => {
setSelectedFilters({});
};
const totalSelected = Object.values(selectedFilters).flat().length;
return (
<aside className="w-72 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-5 space-y-6 shadow-sm">
{/* 标头操作区 */}
<div className="flex items-center justify-between pb-4 border-b border-zinc-100 dark:border-zinc-800">
<div className="flex items-center gap-2">
<SlidersHorizontal className="w-4 h-4 text-zinc-700 dark:text-zinc-300" />
<h2 className="text-base font-semibold text-zinc-900 dark:text-zinc-100">分面筛选</h2>
{totalSelected > 0 && (
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-blue-50 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400">
{totalSelected}
</span>
)}
</div>
{totalSelected > 0 && (
<button
onClick={handleClearAll}
className="text-xs text-zinc-500 hover:text-rose-600 dark:hover:text-rose-400 transition-colors flex items-center gap-1"
>
<RotateCcw className="w-3 h-3" /> 清空
</button>
)}
</div>
{/* 已选条件胶囊折行展示区 */}
{totalSelected > 0 && (
<div className="flex flex-wrap gap-1.5 pb-2">
{Object.entries(selectedFilters).map(([sectionId, options]) =>
options.map(optionId => {
const label = mockFacets.find(s => s.id === sectionId)?.options.find(o => o.id === optionId)?.label;
return (
<span
key={`${sectionId}-${optionId}`}
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 border border-zinc-200/60 dark:border-zinc-700/60"
>
{label}
<button
onClick={() => handleRemoveFilter(sectionId, optionId)}
className="hover:text-rose-500 focus:outline-none"
aria-label={`移除 ${label} 筛选`}
>
<X className="w-3 h-3" />
</button>
</span>
);
})
)}
</div>
)}
{/* 属性分组手风琴列表 */}
<div className="space-y-4 divide-y divide-zinc-100 dark:divide-zinc-800">
{mockFacets.map(section => {
const isOpen = Boolean(openSections[section.id]);
const selectedCount = (selectedFilters[section.id] || []).length;
return (
<div key={section.id} className="pt-4 first:pt-0">
<button
onClick={() => toggleSection(section.id)}
className="w-full flex items-center justify-between py-1 text-left group focus:outline-none"
aria-expanded={isOpen}
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-800 dark:text-zinc-200 group-hover:text-blue-600 transition-colors">
{section.title}
</span>
{selectedCount > 0 && (
<span className="w-1.5 h-1.5 rounded-full bg-blue-600" />
)}
</div>
<ChevronDown
className={`w-4 h-4 text-zinc-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
</button>
{/* 手风琴展开选项 */}
{isOpen && (
<div className="pt-3 space-y-2.5">
{section.options.map(opt => {
const isChecked = (selectedFilters[section.id] || []).includes(opt.id);
return (
<label
key={opt.id}
className="flex items-center justify-between text-sm cursor-pointer group select-none"
>
<div className="flex items-center gap-2.5">
<input
type="checkbox"
checked={isChecked}
onChange={() => handleToggleOption(section.id, opt.id)}
className="w-4 h-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500"
/>
<span className={`transition-colors ${isChecked ? 'font-medium text-zinc-900 dark:text-zinc-100' : 'text-zinc-600 dark:text-zinc-400 group-hover:text-zinc-900'}`}>
{opt.label}
</span>
</div>
<span className="text-xs text-zinc-400 tabular-nums">({opt.count})</span>
</label>
);
})}
</div>
)}
</div>
);
})}
</div>
</aside>
);
};
总结与设计系统工程演进 #
一个卓越的多维分面筛选系统,能够让用户在浩瀚的信息海洋中如臂使指。在设计系统实践与交付中:
- 结构解耦同构复用:桌面端侧栏与移动端抽屉共享同一套属性手风琴与已选 Tag 原子组件;
- Auto Layout Wrap 弹性防截断:确保无论筛选多少维度,已选胶囊都能自适应优雅折行;
- Variables 双向状态机:利用计数变量与布尔变量控制显隐与徽标数字,构建准代码级真实原型;
- URL 路由解耦与无障碍支持:在 Dev Mode 交付中断开与局部临时状态的绑定,推动前端直接对接 URL SearchParams 与标准语义。
深入学习相关高级组件架构实战,欢迎查阅站内 Figma 变量 Variables 系统架构指南、Figma 组件与变体配置实战、Design Tokens 标准命名与结构对照手册、Figma 表单实时校验与即时错误反馈系统组件架构实战 以及 Figma 常见报错与疑难故障诊断手册。
核心手册与速查工具
高频手册与实用设计生产力工具直达常见问题与答疑 (FAQ)
高频疑难解析与避坑指南完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。