Modal 模态框
模态对话框。Teleport + ESC + 点击遮罩 + 滚动锁定四要素。
浮层组件的四个共同要素
Modal、Drawer、Dropdown、Tooltip、ContextMenu 属于同一类组件。 它们都建立在四个机制之上——少了任何一个都会出问题, 所以先说清楚,后面几篇不再重复。
1. Teleport 到 body
浮层若留在组件树里,会被任意一个带 transform / filter 的祖先 创建出的 stacking context 困住。此时 z-index 只在那个局部语境里比较, 页面外层随便一个定位元素都能压住它。
Teleport 让浮层脱离祖先链,z-index 才有全局可比性。
为什么"层级调到最高"往往没用
开发者遇到浮层被遮住,第一反应是把 z-index 调到 99999。如果浮层还在 stacking context 里面,这个数字再大也没用——它只影响同一语境内的比较。
先确认 Teleport,再考虑层级。
2. 层级走令牌,不写数字
所有浮层用 --sr-z-* 系列令牌,形成一个明确的次序:
| 令牌 | 用途 |
|---|---|
--sr-z-dropdown | 下拉菜单 |
--sr-z-sticky | 吸顶元素 |
--sr-z-modal | 模态框、抽屉 |
--sr-z-popover | 需要压过模态框的浮层(如模态内的下拉) |
--sr-z-toast | 全局提示 |
写死数字的问题是:某个组件觉得"我要更高一点"就改成 9999, 然后另一个组件改成 10000——最后没人知道谁在上面。
3. ESC 关闭与焦点归还
打开时焦点移入浮层,关闭时焦点要还给触发它的那个元素。
这一点常被忽略:不归还的话,键盘用户关闭弹窗后焦点丢失, 需要从页面开头重新 Tab 一路找回来。
4. 滚动锁定
模态框打开时锁住背景滚动,关闭后恢复原来的滚动位置。
实现要点:不能简单地 overflow: hidden——那会让页面跳到顶部 (浏览器把滚动位置重置了)。正确做法是记住当前 scrollTop, 用 position: fixed + 负的 top 把它"顶"在原位。
基础用法
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<SrButton variant="primary" @click="visible = true">打开模态框</SrButton>
<SrModal v-model="visible" title="模态框标题">
模态框内容
</SrModal>
</template>关闭方式
<SrModal v-model="visible" close-on-overlay close-on-escape />
<SrModal v-model="visible" :close-on-overlay="false" /> <!-- 必须点按钮 -->表单弹窗建议关掉 closeOnOverlay
用户填了一半误点遮罩,内容就没了。不可逆的数据录入场景(提交申请、 填写资料)应关掉遮罩关闭,让关闭动作必须是明确的。
closeOnEscape 同理——但 ESC 是通行的"取消"语义, 保留它更符合键盘用户预期。折中做法是保留 ESC、关掉遮罩。
尺寸与位置
<SrModal v-model="visible" size="sm" />
<SrModal v-model="visible" size="xl" />
<SrModal v-model="visible" align="top" />
<SrModal v-model="visible" width="720px" />| 尺寸 | 宽度 |
|---|---|
sm | 400px |
md | 520px |
lg | 680px |
xl | 880px |
需要精确控制时用 width。
何时用 align="top"
内容高度超过视口时,垂直居中的弹窗会把顶部内容推到视口外, 用户看不到标题、也不知道该往上滚(因为没出现滚动条)。
align="top" 配合内边距,长内容会自然产生滚动。
遮罩变体
<SrModal overlay="default" />
<SrModal overlay="light" />
<SrModal overlay="none" />overlay="none" 时遮罩不可见但仍然拦截点击—— 适合"弹窗只是补充信息,不希望用户误操作背景"的场景。 若想让背景可操作,应该用 Drawer 的 :overlay="false"。
无底部
show-footer="false" 时不渲染底部区域,页面内自己控制。
<SrModal v-model="visible" :show-footer="false" />Props
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | boolean | false | 是否显示 |
title | string | — | 标题,也可用 header 插槽 |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 尺寸 |
width | string | — | 自定义宽度,优先于 size |
align | 'center' | 'top' | 'center' | 垂直位置 |
closeOnOverlay | boolean | true | 点击遮罩是否关闭 |
closeOnEscape | boolean | true | ESC 是否关闭 |
closable | boolean | true | 是否显示关闭按钮 |
lockScroll | boolean | true | 是否锁定背景滚动 |
showFooter | boolean | true | 是否显示底部 |
overlay | 'default' | 'light' | 'none' | 'default' | 遮罩样式 |
插槽
| 名称 | 说明 |
|---|---|
default | 内容区 |
header | 自定义头部,覆盖 title |
footer | 底部区域(按钮组) |
事件
| 事件 | 说明 |
|---|---|
update:modelValue | 显隐变化 |
open / close | 打开 / 关闭 |
ok / cancel | 点击底部确定 / 取消 |
无障碍
- 容器带
role="dialog"与aria-modal="true",读屏软件会进入"模态"模式 - 通过
aria-labelledby关联到标题,进入时朗读出弹窗的用途 - 打开时焦点移入(通常是第一个可聚焦元素或弹窗本身), 且焦点不会跑到弹窗外——Tab 到末尾会回到开头
- 关闭后焦点归还给触发元素(见上文第 3 点)
- 背景内容加
aria-hidden="true",避免读屏软件读到被遮住的内容