普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月20日首页

深入理解 :focus-within 与 :focus —— 从 CSS 到事件监听再到 React Hook 实践

2025年8月20日 10:37

在日常前端开发中,大家对 :hover:active:focus 等伪类应该都很熟悉。但 :focus-within 可能就没那么常用了,它在表单、交互容器的高亮效果上非常实用。本文将从基础对比出发,逐步深入事件监听,并分享一个 React Hook 版的 useFocusWithin 实现。


一、:focus:focus-within 的区别

:focus

  • 作用:匹配当前获得焦点的元素
  • 常见场景:输入框高亮、按钮键盘操作样式。
input:focus {
  border-color: #1a8cff;
  outline: none;
}

当输入框获得焦点时,边框变蓝。

:focus-within

  • 作用:匹配自身或其子元素中任意一个获得焦点的元素
  • 常见场景:整个容器在子元素聚焦时高亮。
.form-group:focus-within {
  border: 2px solid #1a8cff;
}

即使 form-group 自身不可聚焦,只要里面的 input 获得焦点,容器也会应用样式。

✅ 总结:

  • :focus → 针对当前元素。
  • :focus-within → 针对容器,包含子元素聚焦的情况。

二、相关事件监听

CSS 可以很方便地使用 :focus:focus-within,但有时我们需要 用 JS 捕获对应的状态,比如做动画、逻辑处理。

基础事件

  • focus / blur → 元素自身获得/失去焦点。
  • focusin / focusout → 与 focus 类似,但会冒泡。

因为 :focus-within 的核心就是“容器内是否有元素获得焦点”,所以用 focusin / focusout 来模拟最合适。

封装一个 onFocusWithin

function onFocusWithin(element, onEnter, onLeave) {
  if (!element) return;

  element.addEventListener("focusin", () => onEnter?.(element));

  element.addEventListener("focusout", () => {
  // 延迟一下,避免切换子元素时误判
    setTimeout(() => {
      if (!element.contains(document.activeElement)) {
        onLeave?.(element);
      }
    }, 0);
  });
}

使用:

onFocusWithin(
  document.querySelector(".form-group"),
  () => console.log("进入 focus-within"),
  () => console.log("离开 focus-within")
);

三、React Hook 实现 useFocusWithin

在 React 中,如果每次都手动绑定/解绑事件很麻烦,所以我们可以抽象成一个 Hook。

Hook 实现

import { useEffect, useRef, useState } from "react";

export function useFocusWithin({ onEnter, onLeave } = {}) {
  const ref = useRef(null);
  const [hasFocusWithin, setHasFocusWithin] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const handleFocusIn = () => {
      setHasFocusWithin(true);
      onEnter?.(el);
    };

    const handleFocusOut = () => {
      setTimeout(() => {
        if (!el.contains(document.activeElement)) {
          setHasFocusWithin(false);
          onLeave?.(el);
        }
      }, 0);
    };

    el.addEventListener("focusin", handleFocusIn);
    el.addEventListener("focusout", handleFocusOut);

    return () => {
      el.removeEventListener("focusin", handleFocusIn);
      el.removeEventListener("focusout", handleFocusOut);
    };
  }, [onEnter, onLeave]);

  return [ref, hasFocusWithin];
}

使用示例

function MyComponent() {
  const [ref, hasFocusWithin] = useFocusWithin({
    onEnter: () => console.log("进入 focus-within"),
    onLeave: () => console.log("离开 focus-within"),
  });

  return (
    <div
      ref={ref}
      style={{
        padding: "10px",
        border: hasFocusWithin ? "2px solid #1a8cff" : "2px solid #ccc",
      }}
    >
      <input placeholder="输入内容..." />
      <button>按钮</button>
    </div>
  );
}

inputbutton 获得焦点时,整个容器边框变蓝;失去焦点时恢复灰色。


四、总结

  • :focus 针对单个元素。
  • :focus-within 可感知容器内子元素的焦点状态。
  • JS 中可以用 focusin / focusout 来模拟。
  • 在 React 中,可以封装成 useFocusWithin Hook,方便在项目中复用。

这样一来,我们就能同时利用 CSS 的伪类优势JS 的逻辑控制,打造更灵活的交互体验。

❌
❌