- UI组件
- 前端
【免费下载链接】react-modal
Accessible modal dialog component for React
react-modal 是一个以无障碍(Accessibility)为第一优先级的 React 弹窗(Modal Dialog)组件,遵循 WAI-ARIA 规范,在提供功能完备、可直接用于生产环境的弹窗能力的同时,保证屏幕阅读器等辅助技术用户可以获得与其他用户一致的体验。本文以仓库 docs/index.md 的 API 说明为骨架,结合 Modal.js 与 ModalPortal.js 的源码实现,系统讲解安装方式、全部 Props 的语义与默认值、自定义父节点、Ref 回调,以及 ARIA 与焦点管理背后的真实执行逻辑,读完即可在项目中正确地接入并配置 react-modal。
安装方式
react-modal 支持通过 npm 或 yarn 安装稳定版本:
$ npm install react-modal $ yarn add react-modal在 React 的 CDN 应用中,需要在 React 的 CDN 脚本之后、你自己的 JS 文件之前引入 CDN 脚本,然后在应用中使用<ReactModal>标签:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.14.3/react-modal.min.js" integrity="sha512-MY2jfK3DBnVzdS2V8MXo5lRtr0mNRroUI9hoLVv2/yL3vrJTam3VzASuKQ96fLEpyYIT4a8o7YgtUs5lPjiLVQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>从仓库 package.json 的 peerDependencies 可以看出,react-modal 支持 React 0.14 至 19 的广泛版本范围(react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19)。在 React 16 及以上版本中,组件通过ReactDOM.createPortal挂载弹窗内容(见 Modal.js 中的getCreatePortal逻辑);在更早版本中则回退到ReactDOM.unstable_renderSubtreeIntoContainer。
通用用法与全部 Props 详解
react-modal 唯一必填的 prop 是isOpen,它决定弹窗是否显示。下面是一个指定了全部可用 props 与选项的完整示例(来自 docs/index.md 的 General Usage 部分):
import ReactModal from 'react-modal'; <ReactModal isOpen={false /* Boolean 描述弹窗是否应该显示 */} onAfterOpen={handleAfterOpenFunc /* 弹窗打开后执行的回调函数 */} onAfterClose={handleAfterCloseFunc /* 弹窗关闭后执行的回调函数 */} onRequestClose={handleRequestCloseFunc /* 弹窗被请求关闭时执行的回调(点击 overlay 或按 ESC 触发)。 注意:通过其他方式改变 isOpen 不会调用它。 */} closeTimeoutMS={0 /* 关闭弹窗前等待的毫秒数 */} style={{ overlay: {}, content: {} } /* 弹窗样式对象,包含 overlay 与 content 两个键。 详见 Styles 相关章节。 */} contentLabel="Example Modal" /* 字符串,屏幕阅读器对内容容器的可访问名称 */} portalClassName="ReactModalPortal" /* 应用于 portal 的 className */} overlayClassName="ReactModal__Overlay" /* 应用于 overlay 的 className */} id="some-id" /* 应用于内容 div 的 id */} className="ReactModal__Content" /* 应用于弹窗内容的 className */} bodyOpenClassName="ReactModal__Body--open" /* 应用于弹窗 ownerDocument.body 的 className (必须是常量字符串)。设为 null 时不会给 document.body 添加任何类。 */} htmlOpenClassName="ReactModal__Html--open" /* 应用于弹窗 ownerDocument.html 的 className (必须是常量字符串)。默认值为 null。 */} ariaHideApp={true /* Boolean,指示是否隐藏 appElement */} shouldFocusAfterRender={true /* Boolean,指示渲染后弹窗是否应获得焦点 */} shouldCloseOnOverlayClick={true /* Boolean,指示点击 overlay 是否关闭弹窗 */} shouldCloseOnEsc={true /* Boolean,指示按 ESC 键是否关闭弹窗。 注意:禁用 ESC 关闭弹窗可能引入无障碍问题。 */} shouldReturnFocusAfterClose={true /* Boolean,指示弹窗关闭后是否将焦点恢复到 显示之前获得焦点的元素。 */} role="dialog" /* 字符串,指示弹窗的角色,默认值为 "dialog"。 */} preventScroll={false /* Boolean,指示恢复焦点时是否使用 preventScroll 标志。 */} parentSelector={() => document.body /* 函数,被调用以获取弹窗要挂载到的父元素。 */} aria={{ labelledby: "heading", describedby: "full_description" } /* 附加 ARIA 属性(可选)。 */} data={{ background: "green" } /* 附加 data 属性(可选)。 */} testId="" /* 字符串,渲染><Modal // ... parentSelector={() => document.querySelector('#root')}> <p>Modal Content.</p> </Modal>需要特别注意:如果这样做,请务必正确设置 app element。app element不应是弹窗的父元素,否则弹窗打开时其内容会被屏幕阅读器隐藏。
从源码看,当parentSelector返回值在运行时发生变化时,组件会通过getSnapshotBeforeUpdate捕获新旧父节点(Modal.js),并在componentDidUpdate中把 portal 容器从旧父节点移动到新父节点(Modal.js)。卸载时若父节点已不存在,会输出警告提示避免内存泄漏(Modal.js)。
Refs:获取 Overlay 与 Content 的 DOM 节点
你可以使用 ref 回调直接获取 overlay 和 content 的 DOM 节点:
<Modal // ... overlayRef={node => (this.overlayRef = node)} contentRef={node => (this.contentRef = node)}> <p>Modal Content.</p> </Modal>在 ModalPortal.js 中,setOverlayRef与setContentRef会先将节点保存到实例属性this.overlay/this.content,再转发给用户传入的overlayRef/contentRef回调。这两个内部引用还被用于:onAfterOpen回调参数中的overlayEl/contentEl(见 ModalPortal.js)、点击 overlay 时的焦点重定向(focusContent)以及 Tab 键焦点圈定(scopeTab(this.content, event))。
样式系统:内联样式与 CSS 类
默认内联样式与合并规则
通过styleprop 传入的样式会与默认样式合并。默认样式定义在Modal.defaultStyles对象中(Modal.js):
<Modal ... style={{ overlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.75)' }, content: { position: 'absolute', top: '40px', left: '40px', right: '40px', bottom: '40px', border: '1px solid #ccc', background: '#fff', overflow: 'auto', WebkitOverflowScrolling: 'touch', borderRadius: '4px', outline: 'none', padding: '20px' } }} ... >在 ModalPortal.js 的渲染逻辑中,contentStyles与overlayStyles的选取遵循如下规则:指定了className则禁用 content 的默认样式,指定了overlayClassName则禁用 overlay 的默认样式,之后通过{ ...defaultStyles.xxx, ...this.props.style.xxx }的展开合并方式将自定义内联样式覆盖到默认样式之上(见 ModalPortal.js)。
你也可以直接修改Modal.defaultStyles来更改全局默认样式。默认样式的完整定义可见 docs/styles/index.md 与 Modal.js。
使用 CSS 类控制样式
关于className/overlayClassName的详细用法,请参阅 docs/styles/classes.md,其核心规则如下:
- 每个 prop 可以是单个字符串(应用于对应组件),也可以是一个包含
base、afterOpen、beforeClose三个键的对象。base:始终应用于组件;afterOpen:弹窗打开后应用;beforeClose:弹窗被请求关闭后应用(如用户按 ESC 或点击 overlay)。
beforeClose类只有在closeTimeoutMS设置为非零值时才有效果,否则弹窗被请求关闭时会立即关闭。因此若要利用afterOpen/beforeClose实现过渡动画,应把closeTimeoutMS设为关闭过渡动画的时长(毫秒)。- 指定
className后,默认 content 样式 不再应用;指定overlayClassName后,默认 overlay 样式不再应用。 - 若未指定类名,overlay 会应用默认类
ReactModal__Overlay、ReactModal__Overlay--after-open、ReactModal__Overlay--before-close,content 使用对应的ReactModal__Content前缀。这些默认类上附加的样式不会覆盖默认内联样式(与通过className/overlayClassName指定类时的行为不同)。
这一行为在源码中体现为buildClassName方法(ModalPortal.js):当传入对象时使用对象的base/afterOpen/beforeClose,否则使用默认类名常量(CLASS_NAMES定义于 ModalPortal.js),并根据afterOpen/beforeClose状态追加对应的后缀类。
document.body 与 html 标签的类
- 通过
bodyOpenClassName可以覆盖弹窗打开时添加到document.body的默认类,默认值为ReactModal__Body--open。它必须是常量字符串(因为同时打开多个弹窗时,系统需要管理从哪个弹窗的哪个类名),设为null时不添加任何类,也支持用空格分隔同时添加多个类。 - 一个典型用途是打开弹窗时禁止 body 滚动:
.ReactModal__Body--open { overflow: hidden; }htmlOpenClassName用于给<html>标签添加类,默认值为null,规则与bodyOpenClassName相同(必须是常量字符串),可帮助避免打开弹窗时页面滚动到顶部:
.ReactModal__Body--open, .ReactModal__Html--open { overflow: hidden; }- 通过
portalClassName可以给整个 portal 指定类名,默认不对 portal 本身应用任何样式。
在源码中,beforeOpen会向parentDocument.body与parentDocument的 html 元素添加bodyOpenClassName/htmlOpenClassName(ModalPortal.js),afterClose时对称移除(ModalPortal.js)。开发模式下,若这两个类名在运行中被修改,会输出警告,提示可能造成多弹窗场景的意外行为(ModalPortal.js)。
无障碍(Accessibility)特性
react-modal 以 WAI-ARIA 指南为基准实现无障碍支持,完整说明见 docs/accessibility/index.md。
App Element:屏幕阅读器隔离
对屏幕阅读器用户而言,弹窗打开时页面其他内容应通过aria-hidden属性被隐藏。为此应调用Modal.setAppElement并传入标识应用根节点的选择器,例如应用内容位于 ID 为root的元素内时:
Modal.setAppElement('#root');也可以直接传入 DOM 元素:
Modal.setAppElement(document.getElementById('root'));- 使用匹配多个元素的选择器或传入 DOM 元素列表时,所有元素都会被隐藏。注意:如果元素从 DOM 中移除,这个列表不会自动修剪,因此元素结构变化时可能需要重新调用
Modal.setAppElement,或者直接传入实时的 HTMLCollection。 - 如果你已经通过其他方式给应用内容施加了
aria-hidden,可以传入ariaHideApp={false}来避免"未指定 app element"的警告。 Modal.setAppElement不会把 react-modal 嵌入为你的 React 应用的子组件,它只负责提升应用的无障碍性。
从源码看,setAppElement内部调用ariaAppHider.setElement(Modal.js)。ariaAppHider.js 会解析字符串选择器(使用document.querySelectorAll,无匹配时抛出错误),随后hide/show对每个匹配元素施加或移除aria-hidden属性(ariaAppHider.js)。若未设置 app element,validateElement会输出警告,提示使用Modal.setAppElement(el)或设置appElement={el},并说明可通过ariaHideApp={false}选择退出(ariaAppHider.js)。
弹窗打开/关闭时aria-hidden的管理由 ModalPortal.js 完成:每次打开弹窗ariaHiddenInstances计数器加一,只有计数器归零(即所有弹窗都已关闭)时才移除aria-hidden,这保证了多弹窗嵌套场景下的正确性。
键盘导航:焦点圈定与还原
- 弹窗打开时,Tab 键导航会被限制在弹窗内容内的元素之间,避免弹窗外(打开时不可见)的元素意外获得焦点。实现上,
handleKeyDown在检测到 Tab 键时调用scopeTab(this.content, event)(ModalPortal.js),具体的焦点圈定逻辑见 scopeTab.js。 - 默认情况下,弹窗关闭时焦点会恢复到打开前获得焦点的元素;传入
shouldReturnFocusAfterClose={false}可禁用此行为。焦点还原由 focusManager.js 的returnFocus实现:打开时markForFocusLater将当前活动元素压入栈(focusManager.js),关闭时弹出并调用toFocus.focus({ preventScroll }),preventScroll参数由preventScrollprop 控制。 - 弹窗默认可通过 ESC 键关闭,除非传入
shouldCloseOnEsc={false}。禁用该行为可能给键盘用户带来无障碍问题,因此不推荐禁用。
此外,弹窗内容容器默认带有tabIndex="-1"(ModalPortal.js),且focusContent方法会在不偷取内部元素焦点的情况下将焦点聚焦到内容容器(ModalPortal.js)。
ARIA 属性
除了应用到 app element 上的aria-hidden,react-modal 还支持许多其他 ARIA 属性(完整列表见 WAI-ARIA 1.1 规范):
contentLabelprop:当界面上没有可见标签时,用它为弹窗内容提供aria-label。- 若弹窗已有可见文本标签,应通过
ariaprop 以aria-labelledby指定包含标签的元素。 ariaprop 接受一个对象,键为要设置的属性名(不带aria-前缀)。例如一个带标题和较长描述的 alert 弹窗:
<Modal isOpen={modalIsOpen} aria={{ labelledby: "heading", describedby: "full_description" }}> <h1 id="heading">Alert</h1> <div id="full_description"> <p>Description goes here.</p> </div> </Modal>源码层面,ModalPortal.js 的attributesFromObject会把对象键加上aria-/data-前缀后展开到内容容器上:aria对象默认合并了modal: true(即aria-modal="true"),data对象展开为data-*属性,testId则渲染为data-testid(见 ModalPortal.js)。
关闭过渡动画与 closeTimeoutMS 的配合
借助 CSS 类,可以实现弹窗打开与关闭时的过渡动画。将以下 CSS 放入项目样式后,弹窗内容即可实现打开淡入、关闭淡出:
.ReactModal__Overlay { opacity: 0; transition: opacity 2000ms ease-in-out; } .ReactModal__Overlay--after-open{ opacity: 1; } .ReactModal__Overlay--before-close{ opacity: 0; }上述示例会全局作用于所有未通过classNameprop 自定义afterOpen/beforeClose类的弹窗;若要只作用于单个弹窗,可修改类名并将对象形式传给classNameprop(详见 docs/styles/transitions.md)。
为了让过渡动画生效,必须把动画时长告知<Modal />,即:
<Modal closeTimeoutMS={2000} />closeTimeoutMS以毫秒为单位,其值与 CSS(或styleprop)中使用的动画时长需要保持一致。
- 若使用React 16,关闭过渡只能通过用
isOpenprop 切换弹窗可见性来实现,不要对<Modal />做条件渲染。 - 不要这样写:
{ this.state.showModal && <Modal closeTimeoutMS={200} isOpen contentLabel="modal" onRequestClose={() => this.toggleModal()} > <h2>Add modal content here</h2> </Modal> }- 而应这样写:
{ <Modal closeTimeoutMS={200} isOpen={this.state.showModal} contentLabel="modal" onRequestClose={() => this.toggleModal()} > <h2>Add modal content here</h2> </Modal> }原因在于:React Modal 采用了 React 16 的稳定 Portal API(createPortal),而该 API 不允许开发者干预 portal 组件的卸载过程,条件渲染会导致弹窗被立即卸载,beforeClose过渡动画无法执行。源码实现中,关闭时closeWithTimeout会先设置beforeClose: true并记录closesAt时间戳,再通过setTimeout在closeTimeoutMS毫秒后真正完成关闭(ModalPortal.js),buildClassName会在该状态下为 overlay / content 追加--before-close类以触发退出动画;而在closeWithoutTimeout(closeTimeoutMS为 0)时则立即关闭、跳过过渡。
事件回调与关闭语义
onRequestClose是弹窗被"请求关闭"时的回调,触发来源包括点击 overlay 与按 ESC 键,但不会在通过其他方式改变isOpen时被调用。源码中:
- ESC 键:
handleKeyDown在shouldCloseOnEsc为 true 且检测到 Escape 键时调用requestClose(event)(ModalPortal.js); - 点击 overlay:
handleOverlayOnClick在shouldCloseOnOverlayClick为 true 时调用requestClose(ModalPortal.js),并配合 mousedown/mouseup/click 事件链判断点击是否发生在 content 内部,避免误判; requestClose最终只在存在onRequestClose回调时才触发(ModalPortal.js)。
onAfterOpen在弹窗打开后于下一帧触发,回调参数中包含{ overlayEl, contentEl }(ModalPortal.js),方便在打开动画完成后操作 DOM;onAfterClose在关闭流程全部完成、类名移除与焦点还原之后触发(ModalPortal.js)。
本地示例与开发
examples目录包含多种可直接本地运行的基础示例,运行方式为:
$ npm start或
$ yarn start然后浏览器访问localhost:8080(详见 docs/examples/index.md)。仓库中已有的示例包括 simple_usage、nested_modals、multiple_modals、react-router 等,可分别查看弹窗的基本用法、嵌套弹窗与多弹窗场景。相关测试位于 specs 目录(如 Modal.events.spec.js、Modal.style.spec.js),通过npm test(Karma)即可运行验证各行为的正确性。
小结
react-modal 的核心价值在于"把无障碍做好":从Modal.setAppElement对背景内容的aria-hidden隔离,到 Tab 键焦点圈定与关闭后焦点还原,再到aria-modal、aria-label/aria-labelledby的自动装配,每一项都对应 ModalPortal.js 与 src/helpers 中可验证的源码实现。配合本文梳理的默认值与全部 Props 语义,你可以精准控制挂载位置、样式与过渡动画、关闭行为与无障碍细节,在各类 React 应用中构建合规、可用、可测试的弹窗体验。
- UI组件
- 前端
【免费下载链接】react-modal
Accessible modal dialog component for React
相关推荐
React-Modal无障碍开发工具:axe DevTools使用指南
React Modal无障碍开发工具:axe DevTools使用指南 你是否曾因模态框(Modal)的无障碍问题收到用户投诉?是否在上线前反复检查却仍遗漏键盘
UI组件前端shadcn-vue Label 组件完全指南:从安装、源码解析到无障碍表单实践
shadcn vue Label 组件完全指南:从安装、源码解析到无障碍表单实践 导读 Label 是 shadcn vue 中用于为表单控件(输入框、复选框、
UI组件前端Base UI React Autocomplete 组件 API 全解析:从 Root Props 到定位、过滤与无障碍实现
Base UI React Autocomplete 组件 API 全解析:从 Root Props 到定位、过滤与无障碍实现 本文以 Base UI(base
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考