:focus-within 是父元素内可聚焦子元素获焦时样式响应的唯一可靠原生方案,无需JS、语义清晰;需内部有tabindex="0"等真实可聚焦元素,IE不支持。

focus-within 伪类是唯一可靠解法
父元素获得焦点时子元素变样式,不能靠 JavaScript 监听或手动加 class——太重、易漏、不语义。:focus-within 是原生支持的 CSS 伪类,只要父元素内任意可聚焦子元素(比如 <input>、<button>、带 tabindex 的元素)获得焦点,父元素就匹配 :focus-within,你就能直接给它的子元素写样式。
常见错误现象:用 :focus 直接写在父元素上,结果点不到父元素本身(比如 <div> 默认不可聚焦),样式完全不触发;或者用 JS 绑定 focusin 事件再加 class,但没处理 focusout 或冒泡逻辑,导致样式残留。
-
:focus-within要求父元素是包含关系(不是兄弟或祖先),且内部必须有真实可聚焦元素 - IE 完全不支持,Edge 79+、Chrome 60+、Firefox 61+、Safari 15.4+ 支持良好;如需兼容 IE,只能退到 JS 方案
- 不要试图对
<div tabindex="-1">自身设:focus来“模拟”,它无法让后代响应:focus-within—— 必须靠内部真实聚焦目标触发
怎么写才让子元素真正响应父级焦点
关键不是给父元素加样式,而是利用 :focus-within 作为上下文,在它里面选中目标子元素。比如想让输入框获得焦点时,旁边的图标变色:
form:focus-within .icon {
color: #007bff;
}
注意这里 .icon 是 form 的后代,且 form 内必须有可聚焦元素(比如 <input>)。如果 .icon 是绝对定位脱离文档流,或被 display: none,样式仍生效但不可见——这不是 :focus-within 的问题,是布局问题。
立即学习“前端免费学习笔记(深入)”;
- 子选择器(
>)也行,但限制更严:要求子元素必须是直接子级,而:focus-within天然支持任意嵌套层级 - 避免写成
form:focus-within > *这种宽泛规则,性能差,且可能意外覆盖其他组件 - 如果父元素是自定义组件(比如 Web Component),Shadow DOM 内部焦点不会触发外部宿主的
:focus-within,得在 shadow root 内单独写
为什么 tabindex="-1" 不够,还得有可聚焦内容
tabindex="-1" 只让元素能通过 JS .focus() 聚焦,但它自身没有语义上的“可聚焦子项”,所以不会激活父级的 :focus-within。真正触发它的,是用户 Tab 切换或点击进去的元素。
- 一个
<div tabindex="-1"><span>text</span></div>,点击span不会触发div:focus-within—— 因为span默认不可聚焦 - 正确做法:在
div里放一个<input type="text">,或<button>,或<span tabindex="0">(注意是0,不是-1) -
tabindex="0"让元素进入自然 Tab 顺序;tabindex="-1"只用于程序聚焦,不能作为:focus-within的触发源
和 JavaScript 方案比,差在哪
JS 方案(监听 focusin/focusout)能绕过浏览器兼容性,也能做更复杂的逻辑(比如延迟清除、条件判断),但代价明显:
- 每个需要该行为的父容器都要单独初始化,容易漏绑或重复绑定
- 焦点在父子间快速切换时,
focusout和focusin的触发顺序不稳定,容易样式闪动或残留 - 服务端渲染(SSR)或静态 HTML 场景下,JS 未加载完成前,焦点样式完全不可用
- CSS
:focus-within是声明式、零运行时开销、天然 SSR 友好,只要结构对,样式就稳
真正复杂的地方在于:你要确认整个交互链路里,焦点确实落进了那个容器内部——而不是被 preventDefault 拦截了,也不是被 pointer-events: none 或 visibility: hidden 阻断了可访问性。这点容易被忽略,但一出问题,:focus-within 就像没写一样。










