关于 modal-and-overlay-patterns
为界面设计提供覆盖层模式指南,帮助设计师和前端开发者按任务选择提示框、抽屉或模态框,并统一交互与无障碍规则。
主要功能
- 01按阻断程度选择覆盖层
- 02规范提示框与弹出层
- 03区分下拉选项和操作菜单
- 04制定移动端底部面板规则
- 05说明模态框关闭与堆叠行为
- 06覆盖媒体查看器缩放交互
适用场景
- 01设计确认删除等阻断流程
- 02为移动界面选择导航面板
- 03统一产品中的弹出界面样式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dembrandt/ dembrandt-skills --skill modal-and-overlay-patterns安装后在 Claude Code 中输入/modal-and-overlay-patterns确认已加载。
详细介绍
模态框与叠加层模式
叠加层显示在主内容层之上。它们涵盖从轻量级弹出提示(非阻塞,锚定到触发元素)到完全阻塞的模态框(应用继续运行前需要用户响应)等不同类型。为任务选择合适的叠加层类型,可以避免不必要的打断,并帮助用户保持方向感。
叠加层层级
选择满足任务需求的最轻量级类型。越重的叠加层,认知成本越高。
| 类型 | 阻挡背景 | 锚定到触发元素 | 典型内容 | 关闭方式 |
|---|---|---|---|---|
| 工具提示 | 否 | 是 | 1–2 行说明文字 | 光标移开/失去焦点 |
| 弹出面板 | 否 | 是 | 简短的交互内容:表单字段、选择器、小型列表 | 点击外部区域、按 Escape、显式关闭 |
| 下拉菜单/菜单 | 否 | 是 | 操作或选项列表 | 点击外部区域、按 Escape、选择项目 |
| 底部面板(移动端) | 部分(背景变暗) | 否 | 小屏幕上的操作或内容 | 向下滑动、点击遮罩、按 Escape |
| 抽屉/侧边面板 | 部分(背景变暗) | 否 | 次要编辑、详情视图、长表单 | 按 Escape、显式关闭;也可点击遮罩 |
| 对话框/模态框 | 是(完整遮罩) | 否 | 阻塞式任务:确认操作、填写必填表单 | 按 Escape(仅限非破坏性操作)、点击显式按钮 |
| 全屏叠加层 | 是(完全阻挡) | 否 | 沉浸式任务:媒体查看器、复杂配置 | 仅显式关闭 |
决策规则: 如果用户可以在叠加层打开时继续使用应用的其他部分,请使用非阻塞类型(抽屉、弹出面板)。如果应用必须等待用户响应,请使用模态框。
整个组件系列共用一个外壳
工具提示、弹出面板、抽屉、底部面板和对话框属于同一个组件系列,而不是五个独立组件。它们共用一个外壳:相同的凸起表面、一道极细边框、同一种圆角,以及随模态程度增强的阴影。带有页眉和页脚的组件还共用内部结构:用于标明对象的标题、页眉下方的一道极细分隔线,以及统一的页脚顺序。它们之间只有尺寸、锚定方式(箭头指向或居中)和阻塞性不同。
如果每种叠加层各自设置样式,用户面对的就是同一类信息的不同表达方式:圆角、按钮顺序和确认标签各不相同。每一种差异都需要用户花时间重新熟悉,也更容易让人误读破坏性操作的确认信息。统一外壳也意味着共用一个代码基础组件和一个设计工具组件系列,因此修复可以应用到所有地方。
添加前先审查。 列出产品中的每个浮动界面及其使用的设计令牌。将外壳提取为一个基础组件,并以此构建各类叠加层。通常会发现,确认弹出面板和对话框的页脚顺序不一致,或者对话框页眉仍保留着其他界面早已弃用的粗分隔线。
工具提示
工具提示在悬停或键盘聚焦时出现,并在触发元素失去焦点时消失。它只用于提供信息,内部不能包含交互元素。
- 内容:一句简短的话、标签或键盘快捷键。切勿在工具提示内放置链接或按钮。
- 延迟:悬停时延迟 300–400ms;键盘聚焦时不延迟。
- 位置:优先显示在触发元素上方;接近视口边缘时自动翻转方向。
- ARIA:在元素上设置
role="tooltip";在触发元素上设置aria-describedby,指向工具提示的 id。
弹出面板
弹出面板锚定到触发元素,但包含交互内容,例如颜色选择器、日期范围选择器、小型表单或筛选条件列表。与工具提示不同,用户交互时它会保持打开状态。
- 最大宽度:280–360px。内容较大时,请使用抽屉。
- 位置:锚定到触发元素;自动翻转方向以保持在视口内。
- 关闭方式:点击外部区域、按 Escape,或在内容较长时使用显式关闭按钮。
- 焦点:打开时将焦点移入弹出面板;关闭时将焦点还给触发元素。
- ARIA:如果包含交互内容,使用
role="dialog";如果是列表,使用role="listbox";在触发元素上设置aria-haspopup。
下拉菜单和菜单
下拉菜单会显示可选择的选项或操作,并锚定到触发按钮。它是最轻量的交互式浮层。
- 区分选择下拉菜单(用户选择一个会持续保留的值)和操作菜单(用户触发一项不会作为值持续保留的操作)。
- 宽度:至少与触发按钮等宽;最大不超过 280px。
- 列表较长时:超过 8–10 项时,在顶部添加搜索输入框。
- 键盘操作:按
↑/↓在项目间移动,按Enter选择,按Escape关闭。
底部弹出层(移动端)
在小屏幕上,底部弹出层会取代模态框和弹出层。它从底边向上滑入,触控设备上的使用体验更自然。
- **预览高度:**先显示弹出层的一小部分(拉手和标题),让用户拖动以展开。
- **全屏高度:**适用于较长内容或需要占满视口的表单。
- 关闭方式:向下滑动、点击遮罩层或按
Escape。 - 不要在移动端将对话框置于屏幕中央——改用底部弹出层(居中的模态框尺寸过小且难以触及)。
- ARIA:设为
role="dialog",并采用与模态框相同的焦点管理方式。
抽屉 / 侧边面板
抽屉从左侧或右侧滑入,部分遮挡主内容。适用于次要编辑任务、详情视图或设置等用户使用主内容时可能需要参考的内容。
- **右侧抽屉:**详情视图、编辑表单、筛选/排序面板。最常见。
- **左侧抽屉:**移动端导航(汉堡菜单模式)。
- 宽度:桌面端为 320–480px。移动端为全宽(实际上会变成底部弹出层或全屏覆盖层)。
- 遮罩层:抽屉后方的半透明背景(
rgba(0,0,0,0.4))会使主内容变暗。 - 关闭方式:按 Escape 键,或点击明确的关闭按钮。点击遮罩层关闭为可选操作——抽屉包含未保存的表单时应避免这种方式。
- 如果任务会阻止用户继续操作(例如必须做出决定),不要使用抽屉。应改用模态框。
- ARIA:设置
role="dialog"、aria-modal="true",并将aria-labelledby指向抽屉标题。
模态框 / 对话框
模态框通过全屏遮罩层阻止用户操作整个界面。仅当应用确实无法在用户响应前继续时才使用。
何时使用模态框
- 确认破坏性或不可逆的操作
- 必须在继续前提交的必填表单
- 需要用户确认的错误或警告
何时不使用模态框
- 展示用户可以稍后阅读的信息 → 使用行内提示或通知
- 用户可能想在操作主内容的同时完成的任务 → 使用抽屉
- 包含许多字段的大型表单 → 使用专用页面或抽屉
关闭标签页的情况
beforeunload 会显示浏览器自带的通用对话框,仅此而已。该对话框出现至页面卸载之间不会运行任何脚本,也不会渲染自定义标记。不要围绕此对话框设计“离开前评分”或“检查更改”流程。应针对应用可控制的操作(关闭按钮、路由切换、新搜索)设置拦截,并将关闭标签页视为退出途径。
结构
┌──────────────────────────────────┐
│ Title [✕] │ ← Header: title + close button
├──────────────────────────────────┤
│ │
│ Body content │ ← Content: scrolls if needed
│ (description, form, media) │
│ │
├──────────────────────────────────┤
│ [Cancel] [Confirm]│ ← Footer: actions, right-aligned
└──────────────────────────────────┘
尺寸
| 尺寸 | 宽度 | 适用场景 |
|---|---|---|
| 小 | 360px | 简短确认、单字段提示词 |
| 中 | 480px | 标准对话框、简短表单 |
| 大 | 640px | 多字段表单、内容较丰富的场景 |
| 全屏 | 100% viewport | 沉浸式任务;谨慎使用 |
超出模态框高度的内容应仅在 正文区域 内滚动,标题栏和页脚必须保持可见。
移动优先的对话框应有专属的桌面布局。 在大屏幕上居中的手机宽度卡片会显得局促、不完整:选项换行后挤在狭窄的单列中,交互目标之间的距离也小于所需间距。为桌面断点设置专属宽度、列数和间距,同时在移动端保留原有布局。
每个选项有两三个子选项时,应将其内联显示。 如果选择器中的每个选项都只有少数几个二级选项,就将它们作为始终可见的链接显示在该选项下方。使用折叠面板或另一个对话框会多出一次点击,还会隐藏可选内容;而单独设置第二个维度的选择器,则会让用户选到实际上不存在的组合。
关闭行为
| 触发方式 | 是否允许用于非破坏性操作? | 是否允许用于破坏性操作? |
|---|---|---|
Escape 键 | 是 | 否——必须明确选择取消 |
| 点击遮罩层外部 | 是(可选) | 否——太容易误关闭 |
| 关闭按钮(✕) | 是 | 是 |
| 取消按钮 | 是 | 是 |
对于破坏性或不可逆操作:禁用 Escape 键和点击外部关闭。用户必须明确按下取消或确认。
模态框叠加
避免在模态框中再打开模态框。这表明信息架构存在问题——这项任务可能过于复杂,无法用单个对话框完成。
如果不可避免地需要第二层浮层,请使用锚定在模态框内的弹出层,而不是另一个完整模态框。绝不要叠加两个会阻止交互的遮罩层。
灯箱 / 媒体查看器
灯箱可能没有可见的标题和正文,但仍有交互控件(关闭、翻页和缩放),因此必须是一个带有可访问名称的对话框。关于遮罩层、关闭方式、焦点和文本替代内容的规则仍然适用;缩放和翻页就是全部交互,需要像设计表单一样认真设计。
打开时定位到被点击的项目。 每个缩略图都应让查看器打开到对应的索引,而不是第一张图片。按 Escape 键关闭;打开时将焦点放在关闭按钮上,关闭后将焦点返回缩略图。
根据指针类型选择两种输入模式,而不是根据屏幕宽度。 在运行时通过 matchMedia('(pointer: fine)') 选择默认模式,因为带触摸屏的笔记本电脑和配有触控板的平板电脑都存在。它只会报告主指针,因此应让两种模式同时可用,并根据每个事件的 pointerType 做出判断。
- 精细指针:点击时在指针所在位置切换缩放。可通过移动指针平移(速度快,但用户伸手去点关闭按钮时,图片会随之移动),也可通过拖动平移(操作更从容,但需要多学一个手势)。选定一种后,在整个产品中保持一致。
- 粗略指针:双指捏合缩放、双击切换缩放、拖动平移。这些都是平台教会用户的操作。
不能只靠手势。 显示 + / − 按钮,提供易于发现且可通过键盘操作的缩放方式。当缩放比例为 1× 时禁用 −,达到最大比例时禁用 +,这样按钮也能告知用户当前所处的位置。将 + / − 键和方向键绑定为平移操作,也是对同一规则的合理扩展。
两级缩放。 点击或双击时,在用户触碰的位置跳转到固定的细节级别。按钮则可继续逐级放大,直至硬性上限。具体级别取决于素材:例如,产品摄影可采用 2× 和 4×,地图、扫描件或复杂图表则可以更高。一次直接跳到最大比例,会让用户看到一片模糊像素,却不知道自己身处何处。
光标应提示下一步操作。 在 1× 时显示放大光标,放大后显示缩小光标,在打开查看器的缩略图上显示展开光标。如果缩放实现会在放大后的图片上设置自己的 cursor: move,请覆盖它,否则提示会在交互过程中改变。
区分拖动和平移与点击。 按下后到松开前,如果移动超过几个像素(约 5 px),则应视为平移;松开时不得切换缩放,否则每次平移结束都会让图片跳回 1×。
缩放只对单张图片生效。 用户切换到下一张图片时重置为 1×;图片恢复到 1× 后,应允许滑动再次操作轮播。
固定覆盖层工具的位置。 关闭按钮放在右上角,翻页控件放在底部中央,缩放控件放在右下角。用户只需熟悉一次;如果工具在不同屏幕上移动,这种熟悉就会白费。
播报状态。 每个图标按钮都必须带有 aria-label。除此之外,无障碍的基本要求与任何对话框相同:向屏幕阅读器提供当前缩放比例,可将其写入按钮标签(“放大,当前为 2×”),或使用礼貌播报的实时区域;缩放过渡还应尊重 prefers-reduced-motion。参见 [[wcag-accessibility]]。
焦点管理
每个覆盖层都必须正确管理焦点。焦点管理出错是最常见的无障碍问题之一。
打开时
- 将焦点移到覆盖层内第一个可交互元素上(通常是第一个字段;确认对话框通常是确认按钮)。
- 限制焦点范围:
Tab和Shift+Tab只能在覆盖层内循环;焦点不能移到遮罩层后面的内容上。
关闭时
将焦点返回到触发覆盖层的元素。如果触发元素已不存在(该元素已被删除),则将焦点移到附近逻辑上合适的元素。
实现说明
使用焦点陷阱库,或使用原生 <dialog> 元素;在现代浏览器中,它会原生处理焦点限制。手动实现焦点陷阱容易出错。
模态框和抽屉的 ARIA
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Delete project?</h2>
<p id="modal-description">
This will permanently delete "Apollo" and all its data. This cannot be undone.
</p>
<button>Cancel</button>
<button>Delete</button>
</div>
- 在容器上设置
role="dialog" aria-modal="true"用于告知屏幕阅读器忽略遮罩层后方的内容aria-labelledby指向对话框标题的 idaria-describedby指向描述的 id(可选,但有助于确认操作)- 背景遮罩应设置
aria-hidden="true"——屏幕阅读器不得读取它
破坏性操作确认模式
破坏性操作的确认对话框必须指出操作对象和后果。泛泛地询问“确定吗?”无法提供足够的上下文。
应该这样做:
删除“阿波罗项目”? 这将永久删除所有任务、文件和历史记录,且无法撤销。 [取消] [删除项目]
不要这样做:
确定要执行此操作吗? [否] [是]
- 主要破坏性操作按钮应使用
--color-error/--color-danger,而不是--color-primary。 - 明确标注破坏性操作按钮:“删除项目”“移除成员”“取消订单”,而不只是“确定”或“确认”。
- 取消按钮始终位于左侧(或次要位置);破坏性操作按钮位于右侧。
紧邻单元格上的工具提示
紧邻组中某个单元格的工具提示应在整个组之外打开。当单元格在网格、树图或分段标记中紧邻排列时,应将每个单元格的工具提示放在整个组的上方或旁边,绝不要覆盖相邻单元格:锚定在底行单元格上的工具提示会遮住读者即将指向的那一行。应让工具提示忽略指针,以便移至相邻单元格时能打开下一个工具提示。
审查清单
- 是否为当前任务选择了正确的覆盖层类型(工具提示 / 弹出框 / 菜单 / 底部面板 / 抽屉 / 模态框)?
- 是否避免使用模态框,改用抽屉或行内模式就足够?
- 模态框/抽屉是否有清晰可见的标题、正文和操作按钮?
- 溢出内容是否在正文区域内滚动,同时固定页眉和页脚?
- 对于破坏性操作,是否禁用了 Escape 键(点击外部区域关闭也应禁用)?
- 打开时焦点是否移入覆盖层,关闭时是否返回触发元素?
- 覆盖层打开时,焦点是否被限制在其中?
-
role="dialog"、aria-modal="true"、aria-labelledby是否设置正确? - 破坏性操作确认对话框是否指出操作对象并说明后果?
- 破坏性操作按钮是否有明确标签(而不是“确定”或“确认”)?
- 是否避免了叠加模态框?
- 是否没有围绕
beforeunload设计自定义 UI? - 在移动设备上,是否将模态框替换为底部面板?
- 灯箱:点击/平移交互是否根据指针类型而定,且
+/−按钮和按键是否与手势一起提供? - 灯箱:光标是否显示放大/缩小,且平移操作是否不会在释放时切换缩放?
- 灯箱:页面更改时缩放是否重置,当前缩放比例是否提供给屏幕阅读器?
- 紧邻单元格上的工具提示是否在组外打开,并允许指针移至相邻单元格?
- 所有覆盖层是否共用同一套外壳(表面、边框、圆角、阴影比例)和同一页脚顺序?