What is the CSS :has() “parent selector”
CSS traditionally styles “down the tree”: you select a parent, then style its descendants.
The relational pseudo-class :has() flips that script: it lets you select an element based on what it
contains (or what’s next to it).
That’s why you’ll hear it called a CSS parent selector — because it can style a parent when a certain child
exists.
Officially, :has() is a relational selector from Selectors Level 4, and it matches an element if any
selector inside it matches “relative to” that element.
Browser support is strong in modern browsers, currently sitting at ~93.7% at the time of writing.
Always check current data before shipping something critical.
Can I use’s :has() is the easiest
“single source of truth” for that.
The mental model
Read :has() like this:
-
.thing:has(.child)→ “Select.thingif it has a.childinside.” -
.thing:has(> .child)→ “Select.thingif it has a.childas a direct child.” -
.thing:has(.child:hover)→ “Select.thingif one of its.childelements is hovered.”
The selector inside :has() is often called a relative selector because it’s evaluated “from”
the element you’re selecting.
Learn more about > in the CSS Direct Child Selector (>)
Interactive Tutorial, and learn more about hover interactions in the CSS :hover Pseudo-Class Interactive
Tutorial.
CSS parent selector from child
Let’s start with the classic “style the parent if it contains something”. In this example, the card gets a different outline if it contains a “new” badge, or if it contains an image.
.card:has(.badge) {
outline: 3px solid #ef233c;
}
.card:has(img) {
outline: 3px solid #0077b6;
}
.card:has(.badge):has(img) {
outline: 3px solid #111;
box-shadow: 0 12px 0 #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
}
.card h4 {
margin: 0;
font-size: 18px;
}
.badge {
justify-self: start;
padding: 4px 10px;
border-radius: 999px;
border: 2px solid #111;
font-size: 12px;
background: #f2f2f2;
}
.thumb {
border: 2px solid #111;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16 / 9;
background: #eee;
}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
New Card with a badge
This parent becomes outlined because it has a badge.
![]()
Card with an image
This parent becomes outlined because it contains an image.
New ![]()
Card with both
Two
:has()checks can be combined for precise targeting.
Notice what we did in the last snippet: .card:has(.badge):has(img).
You can chain multiple :has() checks when you want “AND” behavior.
Direct child vs descendant
A super common “why isn’t it working?” moment: you wrote :has(> .thing) but your element is nested
deeper.
The > combinator means “direct child only”.
.panel:has(.flag) {
outline: 3px solid #ef233c;
}
.panel:has(> .flag) {
outline: 3px solid #0077b6;
}
.panel:has(> .meta > .flag) {
outline: 3px solid #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.panel {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
}
.flag {
display: inline-block;
padding: 4px 10px;
border: 2px solid #111;
border-radius: 999px;
font-size: 12px;
background: #f2f2f2;
}
.meta {
display: grid;
gap: 8px;
padding: 10px;
border-radius: 12px;
border: 2px dashed #111;
}
Direct child flag The flag is a direct child of
.panel.
Quick takeaway:
:has(.flag) matches if the flag exists anywhere inside,
while :has(> .flag) matches only if the flag is a direct child.
CSS parent selector class
“Parent selector class” usually means: style a parent if it contains a child with a certain class, or if it contains a child whose class changes (because of state, JS toggles, or your framework).
Here’s a very practical pattern: highlight a list item if it contains an “active” link.
No extra classes on the <li>. No JS.
.menu li:has(a.is-active) {
background: #f2f2f2;
outline: 2px solid #111;
}
.menu:has(a.is-active) .menu-title {
text-decoration: underline;
}
.menu li:has(a.is-active) a {
font-weight: 700;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.menu {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 12px;
}
.menu-title {
margin: 0;
font-size: 18px;
}
.menu ul {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 8px;
}
.menu li {
border-radius: 12px;
padding: 10px 12px;
}
.menu a {
color: #111;
text-decoration: none;
}
.menu a.is-active {
border-bottom: 2px solid #ef233c;
}
This is a great example of “CSS parent selector from child”: the active class lives on the link,
but the styling happens on the parent <li> and even the whole menu.
CSS parent selector examples
Let’s stack a few real-world patterns you’ll actually reuse. The goal: fewer “helper classes” in HTML, and more styling logic that stays in CSS.
Example: style a card if it contains featured content
This card becomes “featured” if it contains an element with .featured.
The markup doesn’t need a separate .card--featured class.
.card:has(.featured) {
border-style: solid;
box-shadow: 0 12px 0 #111;
}
.card:has(.featured) .tag {
background: #111;
color: #fff;
}
.card:has(.featured) .title::after {
content: " ★";
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.card {
border: 3px dashed #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
}
.tag {
justify-self: start;
padding: 4px 10px;
border-radius: 999px;
border: 2px solid #111;
background: #f2f2f2;
font-size: 12px;
display: inline-flex;
justify-content: center;
align-items: center;
}
.title {
margin: 0;
font-size: 18px;
}
.featured {
border: 2px solid #111;
border-radius: 12px;
padding: 10px;
background: #f2f2f2;
}
Normal Regular card
No special content inside, so it keeps the dashed border.
Featured Featured card
This little block triggers the parent styling.
Example: layout changes based on child count
You can use :has() with structural selectors to make layout decisions.
Here, a container switches to 2 columns if it has at least 4 items.
.gallery:has(.item:nth-child(4)) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.gallery:has(.item:nth-child(6)) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gallery:has(.item:nth-child(8)) .item {
font-weight: 700;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.gallery {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 12px;
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.item {
border: 2px solid #111;
border-radius: 14px;
padding: 12px;
background: #f2f2f2;
text-align: center;
}
12345678This is a “CSS parent selector” trick: the parent
.gallerychanges layout based on which children exist.
CSS parent selector hover
Before :has(), “hover the child, style the parent” often meant:
wrap things differently, add classes with JS, or accept a less-perfect interaction.
With :has(), you can do this directly:
.card:has(.button:hover) means “style the card when its button is hovered.”
.card:has(.action:hover) {
transform: translateY(-4px);
box-shadow: 0 14px 0 #111;
}
.card:has(.action:focus-visible) {
outline: 3px solid #0077b6;
outline-offset: 3px;
}
.grid:has(.card:hover) .card {
opacity: 0.65;
}
.grid:has(.card:hover) .card:hover {
opacity: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
box-shadow: 0 10px 0 #111;
}
.card h4 {
margin: 0;
font-size: 18px;
}
.action {
justify-self: start;
border: 2px solid #111;
border-radius: 12px;
padding: 8px 12px;
background: #f2f2f2;
cursor: pointer;
}
Snippet 1
Hover the button to lift the parent.
Snippet 2
Keyboard focus also styles the parent.
Snippet 3
When any card is hovered, dim the others.
Two nice beginner-friendly wins here: hover interactions and keyboard focus can both style the parent, without changing your HTML structure.
Forms and UI state with :has()
:has() shines when a parent needs to reflect a child’s state:
checked, invalid, required, filled, and so on.
Below, each field row becomes “happy” when the checkbox is checked, and “uh-oh” when the input is invalid.
.field:has(input:invalid) {
border-color: #ef233c;
}
.field:has(input:invalid) .hint {
opacity: 1;
}
.field:has(input:valid) {
border-color: #0077b6;
}
.field:has(input[type="checkbox"]:checked) {
background: #f2f2f2;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 720px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.form {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 12px;
}
.field {
border: 3px solid #111;
border-radius: 16px;
padding: 12px;
display: grid;
gap: 8px;
}
label {
display: grid;
gap: 6px;
font-size: 14px;
}
input[type="email"],
input[type="text"] {
border: 2px solid #111;
border-radius: 12px;
padding: 10px 12px;
font: inherit;
}
.row {
display: flex;
align-items: center;
gap: 10px;
}
.hint {
margin: 0;
font-size: 13px;
opacity: 0.35;
}
This is the “parent selector from child” idea again: the state lives on the input, but the styling happens on the container.
Advanced: “parent selector” patterns with siblings
:has() isn’t only about descendants. You can also use it to check for relationships like:
“does this element have a next sibling that matches something?”
This makes it possible to style a heading if it’s followed by a warning block, or style a label if the next element is a help tooltip.
h3:has(+ .notice) {
text-decoration: underline;
}
.section:has(.notice) {
outline: 3px solid #ef233c;
}
h3:has(+ .notice) + .notice {
border-style: solid;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.section {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
}
h3 {
margin: 0;
font-size: 18px;
}
.notice {
border: 3px dashed #111;
border-radius: 14px;
padding: 10px;
background: #f2f2f2;
}
Normal heading
No notice after this one.
Heading with a notice
This notice is immediately after the heading.The heading can detect that it has a
+ .noticesibling.
The key selector here is h3:has(+ .notice):
it selects the h3 if it has an adjacent sibling matching .notice.
Specificity, gotchas, and performance tips
Specificity of :has()
The :has() pseudo-class itself doesn’t add specificity “by itself”.
Its specificity is determined by the most specific selector inside its argument list.
-
.card:has(.badge)is more specific than.cardbecause.badgecontributes. -
If you write
.card:has(#big-deal), that ID selector inside:has()makes it very specific.
Learn more in the CSS Specificity Interactive Tutorial.
Common “why isn’t it working?” checklist
-
You used
>but your element is nested. Try removing>or make your selector match the real structure. -
Your selector inside
:has()never matches. Temporarily style the target directly (the child) to confirm it exists and matches your selector. -
You’re expecting “self matching”.
:has()is about relationships. It’s not meant to match an element based on itself.
Performance tips (so :has() stays your friend)
-
Keep selectors as narrow as possible.
Prefer
.card:has(.badge)overdiv:has(.badge). -
Avoid selecting from the very top.
body:has(...)can be useful, but it’s also the “check everything” option. -
Use
:has()for UI logic, not for every tiny decoration. It’s powerful. It’s also easy to overuse.
Progressive enhancement with :has()
If you need a safe fallback, you can treat :has() as an enhancement:
users with support get the nicer styling, and everyone else gets a perfectly acceptable base design.
A common pattern is to wrap :has() rules in @supports selector(...).
In browsers that don’t understand it, the whole block is ignored.
@supports selector(.card:has(.badge)) {
.card:has(.badge) {
outline: 3px solid #ef233c;
box-shadow: 0 12px 0 #111;
}
}
@supports selector(.card:has(.badge)) {
.card:has(.badge) .badge {
background: #111;
color: #fff;
}
}
@supports selector(.card:has(.badge)) {
.grid:has(.card:hover) .card {
opacity: 0.65;
}
.grid:has(.card:hover) .card:hover {
opacity: 1;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 14px;
display: grid;
gap: 10px;
box-shadow: 0 10px 0 #111;
transition: opacity 160ms ease;
}
.badge {
justify-self: start;
padding: 4px 10px;
border-radius: 999px;
border: 2px solid #111;
background: #f2f2f2;
font-size: 12px;
display: inline-flex;
justify-content: center;
align-items: center;
}
Normal This card has a badge, but no special selector is required for layout.
This card has no badge.
Normal With
@supports, the enhanced styling applies only when supported.
That’s the best beginner mindset for :has():
use it where it makes your HTML simpler and your UI smarter, and keep your base styling solid either way.
Quick reference patterns
-
CSS parent selector has:
.parent:has(.child) -
CSS parent selector from child (direct):
.parent:has(> .child) -
CSS parent selector class:
li:has(a.is-active) -
CSS parent selector hover:
.card:has(button:hover) -
Sibling-based logic:
h3:has(+ .notice) -
Progressive enhancement:
@supports selector(.x:has(.y)) { ... }
CSS Parent Selector :has() Conclusion
The :has() pseudo-class is a game-changer for CSS, enabling “parent selector” patterns that were
previously impossible or required hacks.
With it, you can style elements based on their children, siblings, and even their state.
