<div id="root">
</div>

<div class="blog-note">
  Check the blog post <a href="https://muffinman.io/blog/catching-the-blur-event-on-an-element-and-its-children/" target="_parent">here</a>.
</div>
$blue: #4285f4;

@mixin md {
  @media (min-width: 500px) {
    @content;
  }
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: Helvetica, Arial, sans-serif;
}

.container {
  max-width: 500px;
  margin: 0 auto;
  padding: 40px 20px;

  @include md {
    display: flex;
    justify-content: space-between;
  }
}

.column {
  flex-basis: 25%;
  align-self: center;
}

.group {
  border: 1px solid #eee;
  border-radius: 4px;
  padding: 15px;
  display: flex;
  flex-direction: column;
  margin: 20px 0;
  flex-basis: 50%;
  font-size: 15px;
  color: #ddd;

  @include md {
    margin: 0 20px;
    align-items: center;
  }
  
  button {
    margin-bottom: 10px;
  }
}

.group--focused {
  border-color: $blue;
  box-shadow: 0 0 0 2px $blue;
  color: #111;
}

button {
  display: block;
  width: 100%;
  border-radius: 100px;
  border: none;
  outline: none;
  line-height: 40px;
  max-width: 100px;
  background: #f1f2f6;

  &:hover {
    cursor: pointer;
    box-shadow: 0 0 0 3px #e1e2ea;
  }

  &:focus {
    box-shadow: 0 0 0 3px $blue;
  }
}

.blog-note {
  color: #aaa;
  text-align: center;
  padding: 60px 20px 40px;
  
  a {
    color: #aaa;
  }
  
  a:hover,
  a:focus {
    color: $blue;
  }
}
View Compiled
const { useCallback, useState } = React;

// ----- The main component
const ChildrenBlur = ({ children, onBlur, ...props }) => {
  const handleBlur = useCallback(
    (e) => {
      const currentTarget = e.currentTarget;

      // Give browser time to focus the next element
      requestAnimationFrame(() => {
        // Check if the new focused element is a child of the original container
        if (!currentTarget.contains(document.activeElement)) {
          onBlur();
        }
      });
    },
    [onBlur]
  );

  return (
    <div {...props} onBlur={handleBlur} tabIndex={-1}>
      {children}
    </div>
  );
};


// ----- Demo boilerplate code
const App = () => {
  const [isFocusInElement, setIsFocusInElement] = useState(false);
  
  return (
    <div className="container">
      <div className="column">
        <button className="button button--outside">Outside</button>
      </div>
      
      <ChildrenBlur
        // This will trigger when blur leaves the element and it's children
        onBlur={() => setIsFocusInElement(false)}
        // onFocus works as expected natively
        // but it will be triggered for each focused child element
        onFocus={() => setIsFocusInElement(true)}
        className={`group ${isFocusInElement ? "group--focused" : ''}`}
      >
        <button className="button button--inside">Button 1</button>
        <button className="button button--inside">Button 2</button>
        <button className="button button--inside">Button 3</button>

        Focus is {isFocusInElement ? 'in' : 'out of'} the group.
      </ChildrenBlur>
      
      <div className="column">
        <button className="button button--outside">Outside</button>
      </div>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react/umd/react.development.js
  2. https://unpkg.com/react-dom/umd/react-dom.development.js