先记住这个答案
:focus-within 的匹配范围比 :focus 大。:focus 只匹配直接获得焦点的那个元素,:focus-within 不仅匹配它本身,还匹配它的所有祖先元素。这意味着当用户在 <input> 里打字时,包裹这个输入框的 <div>、<form> 乃至 <body> 都会匹配 :focus-within。
- 后代获焦时祖先也匹配
- 常用于表单容器高亮
- 会一直冒泡到根元素
- 和
:focus可以同时生效
最典型的场景:表单输入时高亮容器
表单设计中经常需要让用户知道「当前在填哪一组」。以前只能用 JavaScript 监听 focus 和 blur 事件,给容器加类名。现在用 :focus-within 一行 CSS 就能搞定。
/* 任何子元素获焦时,给表单组加边框和背景 */
.form-group:focus-within {
border-color: #1890ff;
background-color: #f0f7ff;
}
/* 同时让标签变色 */
.form-group:focus-within label {
color: #1890ff;
}用户点进输入框的时候,.form-group 整个容器会变成蓝色边框加淡蓝背景,标签文字也变蓝。点到其他输入框或者点击空白处,样式自动恢复。焦点在同一个 .form-group 里的不同子元素之间切换时,容器一直保持高亮,不会闪烁。
用 focus-within 做纯 CSS 下拉菜单
:focus-within 还有一个巧妙的用法:做不需要 JavaScript 的下拉菜单。思路是让菜单容器在获得焦点(或者后代获得焦点)时显示子菜单。
.dropdown-menu {
display: none;
}
/* 容器或其后代获焦时显示菜单 */
.dropdown:focus-within .dropdown-menu {
display: block;
}用户用 Tab 键聚焦到下拉按钮时,菜单显示出来。继续按 Tab 切换菜单项时,焦点仍在 .dropdown 的后代里,菜单保持显示。焦点移出整个 .dropdown 后,:focus-within 失效,菜单消失。
这种方式比 :hover 做下拉菜单好,因为它对键盘用户友好。:hover 在手机上还有粘滞问题,:focus-within 没有。
冒泡到根元素
:focus-within 的匹配会一直冒泡到 DOM 树的根元素。焦点在一个深层嵌套的 <input> 上,从 <input> 的直接父元素到 <html>,每一层都匹配 :focus-within。
这个特性可以用来实现一些全局效果。比如页面上有搜索框时,用户开始输入就给整个页面加一层半透明遮罩,引导注意力集中在搜索区域。
不用担心性能问题。浏览器对 :focus-within 的计算做了优化,它只在焦点变化时重新评估,不会像 :hover 那样随着鼠标移动频繁触发。
容易答错的地方
- 以为 focus-within 是继承
:focus-within不是 CSS 继承。继承是父元素的属性值传给子元素,:focus-within是子元素的焦点状态让父元素匹配伪类。方向正好相反:继承从上往下传值,:focus-within从下往上传状态。- 以为焦点在子元素间切换时会闪
- 焦点从一个
<input>移到同一容器内的另一个<input>时,:focus-within不会出现「先消失再出现」的闪烁。浏览器会先让新元素获焦,再让旧元素失焦,中间容器的:focus-within一直成立。
面试官还会怎么问?
focus-within 和 has(:focus) 有什么区别?
功能上几乎一样,:focus-within 等价于 :has(:focus)。但 :focus-within 浏览器支持更早更广,:has() 是后来才普及的。如果只是需要「后代聚焦时父元素匹配」,用 :focus-within 更直接。
focus-within 在可访问性方面有什么好处?
:focus-within 让键盘用户和鼠标用户看到相同的视觉反馈。用户用 Tab 键进入表单组时,容器高亮效果和鼠标点击一样,不需要额外的 JavaScript 适配。这对无障碍访问是天然的加分项。
怎么只让直接子元素聚焦时匹配,不冒泡到更上层?
:focus-within 本身没有「只看直接子元素」的选项,它天然会冒泡到所有祖先。如果只想对直接子元素聚焦做响应,可以用 :has(> :focus) 来精确控制匹配层级。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。