Skip to content

Modal 模态框

模态对话框。Teleport + ESC + 点击遮罩 + 滚动锁定四要素。

浮层组件的四个共同要素

ModalDrawerDropdownTooltipContextMenu 属于同一类组件。 它们都建立在四个机制之上——少了任何一个都会出问题, 所以先说清楚,后面几篇不再重复。

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 把它"顶"在原位。

基础用法

vue
<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>

关闭方式

vue
<SrModal v-model="visible" close-on-overlay close-on-escape />
<SrModal v-model="visible" :close-on-overlay="false" />  <!-- 必须点按钮 -->

表单弹窗建议关掉 closeOnOverlay

用户填了一半误点遮罩,内容就没了。不可逆的数据录入场景(提交申请、 填写资料)应关掉遮罩关闭,让关闭动作必须是明确的。

closeOnEscape 同理——但 ESC 是通行的"取消"语义, 保留它更符合键盘用户预期。折中做法是保留 ESC、关掉遮罩。

尺寸与位置

vue
<SrModal v-model="visible" size="sm" />
<SrModal v-model="visible" size="xl" />
<SrModal v-model="visible" align="top" />
<SrModal v-model="visible" width="720px" />
尺寸宽度
sm400px
md520px
lg680px
xl880px

需要精确控制时用 width

何时用 align="top"

内容高度超过视口时,垂直居中的弹窗会把顶部内容推到视口外, 用户看不到标题、也不知道该往上滚(因为没出现滚动条)。

align="top" 配合内边距,长内容会自然产生滚动。

遮罩变体

vue
<SrModal overlay="default" />
<SrModal overlay="light" />
<SrModal overlay="none" />

overlay="none"遮罩不可见但仍然拦截点击—— 适合"弹窗只是补充信息,不希望用户误操作背景"的场景。 若想让背景可操作,应该用 Drawer:overlay="false"

无底部

show-footer="false" 时不渲染底部区域,页面内自己控制。

vue
<SrModal v-model="visible" :show-footer="false" />

Props

名称类型默认值说明
modelValuebooleanfalse是否显示
titlestring标题,也可用 header 插槽
size'sm' | 'md' | 'lg' | 'xl''md'尺寸
widthstring自定义宽度,优先于 size
align'center' | 'top''center'垂直位置
closeOnOverlaybooleantrue点击遮罩是否关闭
closeOnEscapebooleantrueESC 是否关闭
closablebooleantrue是否显示关闭按钮
lockScrollbooleantrue是否锁定背景滚动
showFooterbooleantrue是否显示底部
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",避免读屏软件读到被遮住的内容

自用组件库 · 源码分发 · 不发布 npm