Material UI focusVisible:v9.4 内建键盘焦点指示器的启用、定制与源码解析

📅 发布时间:2026/9/7 18:14:05
Material UI focusVisible:v9.4 内建键盘焦点指示器的启用、定制与源码解析
Material UI focusVisiblev9.4 内建键盘焦点指示器的启用、定制与源码解析【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-uiMaterial UI 从 v9.4 起在主题层提供了内建的 CSS 键盘焦点指示器focus visible indicator。只需在主题上设置focusVisible: true所有 ButtonBase 派生组件在获得键盘焦点时就会自动渲染统一的焦点环。本文基于官方文档 focus-visible.md 展开覆盖启用方式、内层inset指示器、彩色容器上的双层阴影、三种定制模式与已知限制并逐一对应到packages/mui-material中的实现源码帮助你既会用、也知其原理。一、启用方式与默认指示器启用该功能的方式是把focusVisible: true传给createThemeimport { createTheme } from mui/material/styles; const theme createTheme({ focusVisible: true });默认焦点指示器是一条2 像素的实线 outline颜色取自palette.primary.main向外偏移 2 像素。这一默认值可以直接在源码中确认——focusVisible.ts 中的resolveFocusVisible函数export function resolveFocusVisible( input: true | React.CSSProperties, outlineColor: string, ): React.CSSProperties { return wireFocusVisibleVars({ outlineStyle: solid, outlineColor, outlineWidth: 2, outlineOffset: 2, // invisible shadow for parent component with solid background (AppBar, Snackbar, Alert) can control the ring color. boxShadow: var(${focusVisibleShadowVar}, 0 0), ...(input true ? null : input), }); }可以看到outlineWidth: 2、outlineOffset: 2与文档描述完全一致input传入 CSS 对象时会展开在默认值之后因此后面的定制小节才只需提供差量样式。为什么选择 outline 而非 box-shadow文档给出的理由是CSSoutline是 Web 标准中最常见的焦点指示手段且在包括高对比度high-contrast模式在内的多数环境中都能正常工作。这与源码中为彩色容器准备的隐形 shadowboxShadow: var(--_focusVisible-shadow, 0 0)并不冲突——outline 仍是主指示器box-shadow 只是可被父容器覆写的辅助层。主题解析路径分两条均可在仓库中查证无 CSS 变量路径createThemeNoVars.js 中只要focusVisible不为null/false就调用resolveFocusVisible(muiTheme.focusVisible, muiTheme.palette.primary.main)把指示器颜色解析为当前默认 light方案的primary.main十六进制值CSS 变量路径createThemeWithVars.js 则把默认颜色设为getCssVar(palette-primary-main)即一个 CSS 变量引用。源码注释明确写道这样focusVisible内联展开后可以在 CSS 层按 color scheme 自适应无需像 no-vars 路径那样为每种方案生成一份拷贝。这也意味着在使用 CSS 变量主题时切换 dark/light 模式后焦点环颜色会跟随新方案的primary.main自动变化。官方演示FocusVisibleDefault.js位于 docs/data/material/customization/focus-visible/FocusVisibleDefault.tsx其主题配置值得注意const theme createTheme({ focusVisible: true, colorSchemes: { light: true, dark: true }, // These demos opt out of the ripple, so the focus ring is the only keyboard indicator. components: { MuiButtonBase: { defaultProps: { disableRipple: true } } }, });本页所有演示都通过MuiButtonBase.defaultProps禁用了波纹ripple让焦点环成为唯一的键盘焦点指示便于观察效果。二、内层焦点指示器Inset Focus Indicator像Tab这类组件会把焦点指示器从内部渲染inset以避免被overflow: hidden的滚动容器裁掉或与其他元素重叠。演示见 FocusVisibleInner.tsxconst theme createTheme({ focusVisible: true, colorSchemes: { light: true, dark: true }, components: { MuiButtonBase: { defaultProps: { disableRipple: true } } }, }); // 渲染 Tabs TabTab 的焦点环向内收缩不会被 scroller 裁剪 Tabs value{value} onChange{handleChange} Tab labelOne / Tab labelTwo / Tab labelThree / /Tabs其实现机制在 focusVisible.ts// Clip-prone components (Tab, MenuItem, …) spread this on their root to inset the ring, so an // overflow: hidden ancestor cannot clip it — without the component knowing the ring width. // offset multiplies the rings own outlineOffset, so 1 mirror it inward. export function applyInsetFocusVisible(offset: number) { return { [focusVisibleOffsetVar]: -offset, [focusVisibleBehaviorVar]: inset, }; }关键点在于组件不需要知道自己焦点环的宽度wireFocusVisibleVars会把用户配置的outlineOffset包进calc(var(--_focusVisible-offset) * offset)把自定义的boxShadow前缀上var(--_focusVisible-behavior, )。易被裁剪clip-prone的组件只需在自己的根节点上把这两个私有 CSS 变量翻为内层模式-1/insetoutline 和 box-shadow 就都自动向内收缩。仓库中调用applyInsetFocusVisible(1)的组件包括 Tab、MenuItem、ListItemButton、CardActionArea、Autocomplete、BottomNavigationAction 等即文档所说的显示内层焦点环的完整组件清单——完整清单可以运行页面底部的 FullFocusVisibleDemo 验证。与之相对ButtonBase 的根节点则展开 outsetFocusRing// Spread on a root whose ring must stay outset: the inset vars inherit, so a clip-prone ancestor // would otherwise inset a descendants ring too. export const outsetFocusRing { [focusVisibleOffsetVar]: 1, [focusVisibleBehaviorVar]: initial, // reverts the var to guaranteed-invalid so var(..., ) falls back to empty };源码注释解释了动机内层变量是可继承的若某个易裁剪祖先把变量设为 inset其普通后代如 Tab 内的 Button的环也会被错误地内缩。因此在 ButtonBase.js 的样式中当theme.focusVisible存在时根节点先展开outsetFocusRing把变量复位为保证非法的initial使var(--_focusVisible-behavior, )回退为空再挂上.Mui-focusVisible类的主题环样式保证自己的环始终向外。三、彩色容器上的额外一层 box-shadow当支持键盘焦点的组件渲染在AppBar、Alert、SnackbarContent这类实心彩色背景容器内时仅靠 2px outline 可能在深色面上对比不足。因此focusVisible启用后这三个容器会默认给后代再叠一层 box-shadow 指示器除非你提供了自定义 box-shadow演示见 FocusVisibleColoredSurface.tsx// 演示主题focusVisible: true 关闭 ripple AppBar positionstatic sx{{ borderRadius: 1 }} Toolbar IconButton edgestart colorinherit aria-labelmenu…/IconButton Button colorinheritLogin/Button IconButton colorinherit aria-labeladd…/IconButton /Toolbar /AppBar Alert variantfilled severityerror action{…}Something went wrong/Alert SnackbarContent messageMessage sent action{…} /Tab 之后按钮、图标按钮的焦点环背后会先垫一层背景色 box-shadow再叠加主题 outline从而在彩色面上依然可见。实现是 applyChildrenFocusVisible// Used by the colored-background surfaces (AppBar, Alert, SnackbarContent) to make the focus visible appear through box-shadow export function applyChildrenFocusVisible(color: string) { return { [focusVisibleShadowVar]: color, }; }三个容器的调用点分别为 AppBar.js、Alert.js、SnackbarContent.js。注意 SnackbarContent.js 传入的是0 0 0 4px ${palette.background.default}——一个完整的 shadow 值4px 展开、页面背景色而resolveFocusVisible默认的boxShadow: var(--_focusVisible-shadow, 0 0)在容器外回退为0 0不可见。这就是隐形 shadow、可被父级接管的设计闭环。四、定制 focusVisiblefocusVisible除了true还可以传一个CSS 对象与默认样式做合并...(input true ? null : input)的展开语义。支持的关键键outlineColor、outlineOffset、boxShadow以及标准 outline/box-shadow 属性。4.1 只改 outline 颜色// Recolor only; width and offset stay at the curated 2px. createTheme({ focusVisible: { outlineColor: #9c27b0 } });只改颜色宽度和偏移保持默认的 2px。演示 FocusVisibleRecolor.tsx 即在focusVisible: { outlineColor: #9c27b0 }下渲染按钮。4.2 用 box-shadow 作第二层双色环boxShadow是叠加在 outline 之上的适合做 WCAG 双色环C40 技法内层 outline 外层 box-shadow在任何背景上都保持可见。Material UI 会在内层指示器组件上自动把 box-shadow 内缩因此只需写一个普通的单层值即可处处生效createTheme({ focusVisible: { /* inner indicator */ outlineColor: #F9F9F9, outlineOffset: 0, /* outer indicator */ boxShadow: 0 0 0 4px #193146, }, });演示 FocusVisibleBoxShadow.tsx 实际配置为outlineColor: #193146boxShadow: 0 0 0 4px #FFF并同时展示了Card浅色面与AppBar深色面两种场景浅色面上深色 outline 起作用深色面上白色 box-shadow 起作用。两条源码级约束值得记住boxShadow 只支持单层。逗号分隔的多层值不被支持wireFocusVisibleVars只会给整个值前缀inset变量在内层指示器组件上仅第一层会内缩后面的层全部保持 outset 并被容器裁掉。文档明确要求要双色环就用outlineColorboxShadow的组合而不是叠两层 box-shadow。Button、Fab等自带焦点 box-shadow 的组件会把两层合成——它们保留原有的 focus elevation并把你的 box-shadow 一并渲染在上层。独立关键字值none、initial、inherit、unset、revert、revert-layer不会被前缀处理见 wireFocusVisibleVars 中的standaloneBoxShadows白名单与inset检测逻辑。4.3 用 box-shadow 完全替换 outlinecreateTheme({ focusVisible: { outlineColor: transparent, boxShadow: 0 0 0 3px #1976d2, }, });这里有个重要的无障碍细节请用outlineColor: transparent隐藏 outline而不要用outline: none。在 forced-colors 模式下浏览器会剥掉 box-shadow、并把 outline 强制为系统颜色——此时透明 outline 会复活为系统色的焦点指示器而outline: none会把它彻底移除键盘焦点将没有任何可见指示。这是文档给出的 success 提示也是定制时最容易踩的坑。五、完整组件演示文档最后一节提供了全量演示 FullFocusVisibleDemo.tsx同目录还有 FullFocusVisibleDemo.js 的元数据文件列出启用focusVisible后所有会渲染焦点环的组件用键盘Tab与方向键移动焦点即可看到焦点环。同一目录下的*.preview文件如 FocusVisibleDefault.tsx.preview是各演示的截图元数据可用于快速预览效果。需要说明的是演示中组件的focusVisible状态由 ButtonBase 自身的状态机管理键盘来源的焦点会置位focusVisible并追加.Mui-focusVisible类而 focusWithVisible.js 则负责程序化聚焦时透传该来源export default function focusWithVisible(element, focusSource) { if (focusSource null) { element.focus(); return; } try { element.focus({ focusVisible: focusSource keyboard }); } catch (error) { element.focus(); // 浏览器不支持 focusVisible 选项时退化为普通 focus } }即只有键盘来源的程序化聚焦才会触发可见环鼠标来源不会——这与 CSS:focus-visible的语义保持一致。六、注意事项Caveats6.1 Checkbox / Radio 自定义图标必须是 SVGCheckbox 与 Radio 会把焦点指示器挂到组件内的第一个svg元素上。若通过icon、checkedIcon属性定制图标自定义图标必须渲染svg元素——字体图标或img都不会收到焦点环。由于指示器紧贴 svg 实际渲染的盒子换成更小的图标时环会按比例收紧无需额外调整。官方推荐用SvgIcon包裹自定义 svg 以获得一致的样式。演示 FocusVisibleCustomIcons.tsx 用 16px 的TightSquareIcon/TightCircleIcon展示了这一行为Checkbox icon{TightSquareIcon /} checkedIcon{TightSquareCheckedIcon /} defaultChecked / // Tab 之后焦点环紧贴 16px 的 svg 图标6.2 组件自有的 focus-visible 样式会被主题替换部分组件默认用半透明背景或叠加层表示键盘焦点Chip、MenuItem、ListItemButton、AccordionSummary、PaginationItem、CardActionArea、Autocomplete选项以及Slider滑杆。启用focusVisible后这些组件的 focus-visible 样式会被移除只保留主题指示器从而保证所有组件表现一致hover、selected、active 样式不受影响。这与第一节源码中 ButtonBase 用internalDisabledThemeFocusVisible私有属性区分根节点 outset 环和子组件让位的机制相印证。6.3 改调色板重组主题时记得重传 focusVisible把已创建的主题展开进新的createTheme()并修改 palette 时指示器颜色仍会沿用原palette 解析出的值no-vars 路径下resolveFocusVisible只按调用时的palette.primary.main解析一次。必须在同一次调用中重传focusVisible让颜色按新 palette 重新推导const base createTheme({ focusVisible: true }); // ✅ re-pass focusVisible to re-derive the color from the new palette createTheme({ ...base, palette: { primary: { main: #2e7d32 } }, focusVisible: true, });源码侧的防护见 isResolvedFocusVisible它通过检测outlineOffset是否已包含私有 offset 变量来识别已被解析过的对象wireFocusVisibleVars据此跳过二次包裹避免calc被重复嵌套导致符号反转。CSS 变量主题路径则天然规避了这个问题——颜色是变量引用改 palette 后自动跟随见第二节 createThemeWithVars.js 的注释。七、小结与源码索引主题能力文档行为源码位置默认 2px/2px 实线 outline取primary.mainfocusVisible: truefocusVisible.ts、createThemeNoVars.jsCSS 变量主题下颜色随 color scheme 自适应无需为每种方案拷贝createThemeWithVars.js内层 inset 指示器防裁剪Tab 等自动内缩applyInsetFocusVisible、wireFocusVisibleVars彩色容器额外 shadow 层AppBar/Alert/SnackbarContentapplyChildrenFocusVisible、SnackbarContent.js主题环挂载与状态类.Mui-focusVisibleButtonBase.js键盘来源的程序化聚焦仅键盘触发可见环focusWithVisible.js启用一行配置即可获得跨组件一致、可定制、且在彩色容器与高对比度模式下都有考量的键盘焦点指示器理解focusVisible对象的合并 变量接线机制后上面的定制模式改色、双色环、纯 box-shadow都只是在默认值上做差量覆盖可以放心在生产主题中组合使用。【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考