v149 · css · behavior change
User-Action Pseudo-Class Top-Layer Boundary
Chrome 149 implements the CSS Selectors Level 4 rule that :hover, :active, and :focus-within propagate up the ancestor chain only as far as the nearest top-layer element — so interacting with a popover or dialog no longer accidentally triggers hover/active styles on elements behind it in the regular document flow.
:hover or :focus-within propagating from a popover or dialog into its DOM ancestors, those rules will stop matching in Chrome 149. This is the correct interoperable behaviour per the spec.
at a glance
| Shipped in | Chrome 149 / Edge 149 |
|---|---|
| Status | Enabled by default |
| Flag | None |
| Spec | CSS Selectors Level 4 — user-action pseudo-class propagation rules |
| ChromeStatus | 6296574159355904 — User action pseudo class top layer boundary |
what changed and why
The :hover, :active, and :focus-within pseudo-classes propagate upward through the DOM: when an element matches, all its ancestors match too. This makes sense for regular document flow — hovering a button inside a card should allow you to style the card.
Top-layer elements (popovers, <dialog>s, and fullscreen elements) are special: although they exist in the DOM tree, they are rendered in a separate stacking layer entirely above the rest of the page. When a button inside a popover is hovered, propagating :hover all the way up to the <body> or a container <section> makes no visual sense — those elements are displayed behind the popover and the user is not interacting with them.
CSS Selectors Level 4 specifies that user-action pseudo-class propagation stops at the first top-layer element in the ancestor chain. Chrome 149 implements this rule.
Source: blink-dev Web-Facing Change PSA.the rule
Given this DOM structure:
<main>
<div popover id="panel">
<button>Click me</button>
</div>
</main>
When the user hovers the <button>:
| Element | Before Chrome 149 | Chrome 149+ |
|---|---|---|
<button> |
:hover matches | :hover matches |
<div popover> (top-layer element) |
:hover matches | :hover matches |
<main> (ancestor of the top-layer element) |
:hover matches | :hover does NOT match |
<body>, <html> |
:hover matches | :hover does NOT match |
The same boundary rule applies to :active and :focus-within. It applies to all top-layer promotion types: popover attribute, <dialog> (opened with showModal()), and requestFullscreen().
example
CSS that breaks in Chrome 149
/* This selector will NO longer match when the user hovers a button
inside a popover that is a descendant of .sidebar. */
.sidebar:hover {
background: var(--hover-tint);
}
CSS that still works
/* Hovering within the popover itself still sets :hover on the popover. */
[popover]:hover {
box-shadow: 0 4px 20px rgba(0,0,0,.2);
}
/* Hover on regular (non-top-layer) ancestors still propagates. */
.card:has(button:hover) {
border-color: var(--accent);
}
Feature detecting the new behaviour
// No CSS feature query for this change. Use the :has() workaround
// for styling ancestors of hovered children, which works regardless.
.container:has(:popover-open:hover) {
/* applies when an open popover inside .container is hovered */
}
affected pseudo-classes
:hover | Stops propagating at the first top-layer ancestor. |
|---|---|
:active | Stops propagating at the first top-layer ancestor. |
:focus-within | Stops propagating at the first top-layer ancestor. |
:focus, :focus-visible | Not affected — these match the focused element itself, not its ancestors. |
browser support
| Chrome | 149 |
|---|---|
| Edge | 149 (Chromium) |
| Firefox | Status unknown (as of Chrome 149) |
| Safari | Status unknown (as of Chrome 149) |