The Powerful Tools of CSS: A Detailed Exploration of ":not()" and ":has()" Selectors

Table of contents
- The :not() Pseudo-class: Self-interrogation and Exclusion
- The :has() Pseudo-class: Relationship-interrogation and Selection
- Combination and Chaining of :not() and :has()
- Exercise time
- A Deep Comparative Application of :not() and :has(): A Case Study
- Showing cool stuff
- Key Differences Between :not() and :has()
- Conclusion

In this article, we explore the powerful CSS pseudo-classes `:not()` and `:has()`, which enhance the ability to apply dynamic and precise styling in modern web development. The `:not()` pseudo-class excludes elements based on a specific condition, whereas `:has()` selects elements based on their descendants or subsequent siblings. Through detailed examples and explanations, we'll cover when and how to effectively use these pseudo-classes to achieve complex styling tasks traditionally handled by JavaScript. This guide aims to simplify your CSS coding and reduce dependency on JavaScript for certain tasks.
In modern web development, CSS selectors play an indispensable role in creating dynamic and precise styling. The :not()
and :has()
pseudo-classes have taken CSS to a new level, enabling functionalities that were previously only possible with JavaScript. In this article, we will discuss the functionality and use cases of these two powerful selectors with detailed examples, clarifying when to use each.
The :not()
Pseudo-class: Self-interrogation and Exclusion
The :not()
pseudo-class, also known as the negation or exclusion selector, is used to select elements that do not satisfy a specific condition. Its primary function is to exclude certain elements from an initial list of elements.
Its most crucial characteristic is that the condition inside it applies to the selected element itself; it does not evaluate any element inside or adjacent to it. To easily understand this process, you can think of the :not()
selector as asking each element: "Am I... (fulfilling this specific condition)?"
Syntax:
:not(<selector >) {
/* CSS properties */
}
Basic example
You have a list of items, and you need to change the background color of only those items that do not have the active
class.
Explanation: This code first finds all <li>
elements. Then, the :not(.active)
part asks each <li>
element, "Do I have the active
class?". The <li>
element that has the active
class (the second item) is excluded from the list. As a result, only the first and third items are selected, and their background-color
is changed to brown
.
When is :not()
valid?
The condition inside :not()
is valid when it describes a feature or state that validates a simple pattern, a characteristic of the element itself. Alternatively, it can be a complex, relational pattern that validates the element's position or relationship. In other words, if the selected element has the capability to meet that condition.
Simple selectors inside :not()
The condition inside :not()
can be an element's own class, attribute, ID, or tag name, which describes a pattern.
You have two paragraphs. Set the background-color: blueviolet
for the paragraph that does not have the important
id, and background-color: brown
for the paragraph that does not have the data-set="xyz"
attribute.
Explanation: The first rule, p:not(#important)
, selects all <p>
elements that do not have id="important"
. As a result, the second paragraph gets a background-color: blueviolet
. Similarly, the second rule, p:not([data-set="xyz"])
, selects the <p>
element that does not have the data-set="xyz"
attribute. This causes the first paragraph's background-color
to become brown
.
Complex selectors inside :not()
Here comes the most important and subtle point. The condition inside :not()
can be not just a class, ID, or tag name, but a complete selector that describes a relationship or pattern. :not()
still checks the identity of the selected element, but that identity is not just its name or class, but also its complete pattern or context.
Example 1
You need to select only those <div>
elements that are not direct children of an <a>
element.
Explanation: This code first selects all <div>
elements inside the <li>
(Child 2, 3, 4, 5). Then, :not(a > div)
asks each selected <div>
, "Am I a <div>
that is a direct child of an <a>
element?". The fourth <div>
(Child 4) meets this condition, so it is excluded from the list. The remaining <div>
s (Child 2, 3, 5) do not meet this condition, so their background-color
is changed to brown
.
Example 2
You need to set the background-color: brown
for the <img>
element whose parent is not an <a>
element, and the <img>
element is a direct child of its parent.
Explanation: This selector works step-by-step:
div > ...
: It looks at the direct children of the outer<div>
element (<p>
,<a>
, the inner<div>
, and<img>
).... :not(a) ...
: It excludes the<a>
element from the previous list. Now the list contains<p>
, the inner<div>
, and<img>
.... > img
: Finally, it looks for an element among the elements from the previous step (<p>
,<div>
,<img>
) that has a direct child<img>
.The
<p>
element has a direct child<img>
. So the first<img>
(photoOne.png) is selected.The inner
<div>
element does not have a direct<img>
child (its child is<a>
).Therefore, only the first
<img>
element'sbackground-color
is changed tobrown
.
Chaining :not()
selectors
Multiple :not()
selectors can be used together to create more complex conditions.
You have four <article>
elements. You need to set the background-color: brown
for only the <article>
element that has neither the featured
ID nor the tutorial
class.
Explanation: The article:not(#featured)
part initially selects all <article>
elements that do not have the featured
ID. Then, the :not(.tutorial)
part filters the previously selected list for elements that do not have the tutorial
class.
The first
<article>
element is excluded by the first:not()
condition because it has thefeatured
ID.The second
<article>
element is excluded by the second:not()
condition because it has thetutorial
class.The third
<article>
element is excluded by both:not()
conditions because it has both thefeatured
ID and thetutorial
class.The fourth
<article>
element has neither thefeatured
ID nor thetutorial
class, so it is selected, and itsbackground-color
becomesbrown
.
Note: The syntax article:not(#featured):not(.tutorial)
is equivalent to De Morgan's Laws - NOT (A) AND NOT (B). This condition can also be written as article:not(#featured, .tutorial)
, which is equivalent to De Morgan's Laws - NOT (A OR B). For modern browsers, this syntax is generally preferred because it is more concise and readable. However, if support for older browsers is needed, the chaining method should be used.
Special application: If you want to exclude only the <article>
element that has both the featured
ID and the tutorial
class, you can use article:not(#featured.tutorial)
. This is equivalent to De Morgan's Laws - NOT (A AND B). If you want to style the <article>
element that does not have the featured
ID or does not have the tutorial
class, you can use article:not(#featured), article:not(.tutorial)
. This is equivalent to De Morgan's Laws - NOT (A) OR NOT (B).
When is :not()
invalid?
When the condition inside :not()
describes something that the selected element can never be. In this case, the :not()
filter effectively does nothing because the condition is always true (since the element is not of that type).
Example 1
You need to select the <img>
elements that are not inside an <a>
.
Explanation: This code will not work because the selector img:not(a)
asks each <img>
element: "Are you an <a>
element yourself?". Since an <img>
tag can never be an <a>
tag, this condition is true for all <img>
s. As a result, the :not()
filter becomes meaningless and does not exclude any <img>
element.
Correct ans: li img:not(a > img)
or li img:not(a img)
Example 2
You need to set the background-color: brown
for the paragraph that does not contain any <span>
element.
Explanation: This code is incorrect because :not()
checks the element's own identity, not its content. The selector p:not(span)
asks each <p>
element, "Are you a element?". Since no <p>
element is a <span>
, the condition is true for all paragraphs, and the background color of all of them is changed.
Correct ans: p:not(:has(span))
The :has()
Pseudo-class: Relationship-interrogation and Selection
The :has()
pseudo-class is a revolutionary addition to CSS. Its main purpose is to select an element based on the content within it. It asks a question to find a relationship between the selected element and its descendants or subsequent siblings: "Is there... inside me or next to me?". Because of this capability, :has()
is often called the "parent selector" or, more accurately, the "relational" pseudo-class.
Syntax:
:has(<selector >) {
/* CSS properties */
}
Basic example
You have two <div>
s, you only need to give a brown
border to the <div>
that has an <img>
inside it.
Explanation: This code targets each <div>
element and asks, "Is there an <img>
element inside me?". Since the first <div>
has no <img>
inside, it is not selected. But since the second <div>
has an <img>
inside, the condition is met, and a brown
border is added around that <div>
element.
When is :has()
valid?
The condition inside :has()
is valid when it describes a descendant or a subsequent sibling of the selected element.
Checking descendant elements:
When the style of a parent element depends on its child element.
You need to set the background-color: brown
for the <a>
element that is a direct child of a <div>
and has an <img>
element directly inside it.
Explanation: This selector first finds the <a>
elements that are direct children of a <div>
. Then, the :has(> img)
part filters those <a>
elements and asks, "Do I have an <img>
element as a direct child?".
The first
<a>
element has a direct child<img>
, so it is selected.The second
<a>
element's direct child is a<span>
, not an<img>
, so it is excluded.The third
<a>
element is not a direct child of the<div>
, so it is excluded from the beginning. As a result, only thebackground-color
of the first<a>
element is changed.
Note: If div > a:has(img)
were used, both the first and second <a>
elements would be selected. This is because the div > a:has(img)
selector selects <a>
elements that are direct children of a <div>
and have an <img>
element anywhere inside them (as a direct child or descendant). However, this does not align with our original condition, which required the <img>
element to be a direct child of the <a>
.
Checking subsequent siblings:
This capability of :has()
has brought a revolution to CSS. Traditionally, the +
(Adjacent Sibling) and ~
(General Sibling) combinators could only work "forwards", meaning you could style the next siblings of an element, but not the previous ones. :has()
has broken this limitation.
Example 1
You need to highlight the <p>
element if it is immediately followed by a <blockquote>
element.
Explanation: This code targets each <p>
element and asks, "Is there a <blockquote>
element immediately after me?" (+
means the very next sibling). Only the first <p>
element meets this condition, because a <blockquote>
follows it. The other <p>
elements are not styled because they are not followed by a <blockquote>
.
Example 2
You have a <section>
element. If that <section>
element contains a <table>
, then set the background-color: brown
for all <h2>
headings within that <section>
. Here, the <table>
element can be immediately after the <h2>
or any subsequent sibling.
Explanation: The h2:has(~ table)
selector selects the <h2>
element that has a <table>
element anywhere after it. Here, :has()
asks each <h2>
, "Is there a <table>
after me?". Because the general sibling combinator (~
) is used, the <table>
doesn't have to be immediately after the <h2>
; the condition is met if it's anywhere after. However, (~
) only looks for elements that come after, not before.
The first
<h2>
is selected because there is a<table>
after it.The second
<h2>
is not selected because there is no<table>
after it (as~
doesn't look backward).The
<h2>
in the second section is also not selected as there is no<table>
after it.
Chaining :has()
selectors
You need to set the background-color: brown
for the <h2>
element that is a direct child of a <header>
, where the <header>
contains both an <h2>
element and a <p>
element with the subtitle
class.
Explanation: The header:has(h2)
part selects <header>
elements that have an <h2>
element inside them. Then, :has(p.subtitle)
filters the previously selected <header>
elements to find those that also have a <p>
element with the .subtitle
class. Finally, > h2
selects the direct child <h2>
element of the selected <header>
.
The first
<header>
has an<h2>
, but no<p>
with the.subtitle
class, so it is not selected.The second
<header>
has two<h2>
s, and its first<h2>
heading is a direct child of the<header>
. So, this<h2>
is selected and getsbackground-color: brown
. The second<h2>
is not selected because it is not a direct child of the<header>
(it is a child of the<div>
).
Note: The syntax header:has(h2):has(p.subtitle)
is equivalent to De Morgan's Laws - HAS (A) AND HAS (B), meaning the <header>
must contain both <h2>
and p.subtitle
.
Special application: If you want the background-color: brown
for the direct child <h2>
of a <header>
element if it contains either an <h2>
or a <p>
with the subtitle
class, you would use header:has(h2, p.subtitle) > h2
. This is equivalent to De Morgan's Laws - HAS (A OR B). In this case, the direct child <h2>
elements of both <header>
elements would be selected.
When is :has()
invalid?
When you try to use :has()
to check the state of the element itself or its ancestors. :has()
is not designed to check an element's own properties.
You need to set the background-color: brown
for the <div>
element that has the card
class itself.
Explanation: This code will not work because :has()
looks for elements inside it, not its own class. The selector div:has(.card)
will look for a <div>
that has another element with the .card
class inside it. Since the <div>
itself has the .card
class and there is no such element inside it, it will not be selected.
Correct ans: div.card
(the simplest and correct way)
Combination and Chaining of :not()
and :has()
The combination and chaining of :not()
and :has()
are among the most powerful aspects of CSS selectors. With them, you can create extremely complex and specific conditions that were previously only possible with JavaScript.
Example 1
You need to set the background-color: brown
for the <p>
element that contains no other elements, meaning it is purely text-based.
Explanation: Here, :has(*)
selects all <p>
elements that have any type of child element (*
means any element). In this case, the first and third paragraphs will be selected by :has(*)
. Then, :not()
inverts that selection, meaning it only selects the <p>
elements that have no child elements. As a result, only the second <p>
element's background-color
will be brown
.
Example 2
You need to style the <article>
element that contains an <img>
element but does not have any element with the badge-new
ID.
Explanation: This code first uses article:has(img)
to select <article>
elements that have an <img>
inside (the first and second). Then, :not(:has(#badge-new))
excludes the <article>
from this list that has an element with the #badge-new
ID inside it (the second article). As a result, only the first <article>
remains and its style is changed.
Example 3
You need to style the <div>
element that is not the element with the main-content
ID, but has a <video>
element inside it.
Explanation: :not(#main-content)
first excludes the <div>
element with the main-content
ID. Then, :has(video)
selects from the remaining <div>
elements only the one that has a <video>
inside. As a result, only the sidebar's <div>
element is styled.
Example 4
You need to style the element with the gallery
class that has an <img>
inside it which does not have the featured
class. That is, any one <img>
element without the featured
class will suffice.
Explanation: This code targets the .gallery
elements and asks, "Is there an <img>
inside me that does not have the .featured
class?".
In the first gallery, all
<img>
elements have the.featured
class. So theimg:not(.featured)
condition is not true for any<img>
. As a result, the first gallery is not selected.In the second gallery, two
<img>
elements do not have the.featured
class. Since there are elements inside that meet the condition, the second gallery is selected, and its style is changed.
Exercise time
Here are three exercises for you. Each exercise provides specific conditions and the necessary HTML code. Based on these conditions, you need to apply CSS styles to the relevant elements. Although the solutions are provided below, we encourage you to try them yourself first.
Exercise 1
You have a <form>
element. You need to style only those <input>
elements that are not inside a <div>
with the form-group
class. In other words, select the <input>
elements that are "isolated" or stand-alone.
<form>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" />
</div>
<input type="text" name="search-query" placeholder="Search here..." />
<button type="submit">Submit</button>
</form>
Solution
form > input:not(.form-group)
Exercise 2
You have a <div>
element with a gallery
class. You need to style only those <img>
elements that meet the following conditions:
Not a direct child of an
<a>
element.Does not have the
promo-item
class.Does not have the
data-format="square"
attribute.
<div class="gallery">
<a href="">
<img src="photoOne.png" alt="Photo one" />
</a>
<img src="photoTwo.png" alt="Photo two" class="promo-item" />
<img src="photoThree.png" alt="Photo three" data-format="square" />
<img src="photoFour.png" alt="Photo four" data-format="portrait" />
<img src="photoFive.png" alt="Photo five" />
<a href="">
<img src="photoSix.png" alt="Photo six" class="promo-item" />
</a>
<div>
<img src="photoSeven.png" alt="Photo seven" class="promo-item" />
</div>
<div>
<img src="photoEight.png" alt="Photo eight" data-format="portrait" />
</div>
</div>
Solution
.gallery img:not(a > img):not(.promo-item):not([data-format="square"])
Exercise 3
You have three <article>
elements with the post
class. You need to change the style of the first <p>
element of only that <article>
which does not contain any element with the class call-to-action
or ad-banner
.
<article class="post">
<h2>A Pure Article</h2>
<p>The font size of this paragraph will be larger.</p>
<p>This is the second paragraph, it will remain normal.</p>
</article>
<article class="post">
<h2>An Offer</h2>
<p>This paragraph will remain normal.</p>
<button class="call-to-action">Buy Now</button>
</article>
<article class="post">
<h2>Article with an Ad</h2>
<p>This paragraph will also remain normal.</p>
<div class="ad-banner">Special Discount!</div>
</article>
Solution
article:not(:has(.call-to-action, .ad-banner)) p:first-of-type
A Deep Comparative Application of :not()
and :has()
: A Case Study
Often, when using :not()
and :has()
, we make some common mistakes, especially when we want to filter based on the content inside an element. The following example and comparative analysis will help clear up this confusion and show why some selectors don't work logically, and which one provides the correct solution.
<div>
<a href="">
<img src="photoOne.png" alt="Photo one" />
</a>
</div>
<div>
<a href="">
<span>This is a dummy span element inside an anchor element.</span>
</a>
<img src="photoTwo.png" alt="Photo two" />
</div>
<div>
<p>This is a dummy paragraph inside a div element.</p>
</div>
div:not(a > img) {
background-color: aqua;
}
div:not(a:has(img)) {
background-color: aqua;
}
div:not(:has(img)) {
background-color: aqua;
}
div img:not(a > img) {
background-color: aqua;
}
Final Comparative Analysis of Selectors
Selector | Main Target | Explanation & Logic | Result on your HTML |
div:not(a > img) | div | Logically meaningless. :not() here asks each div , "Do you match the pattern a > img ?" Since a div can never be an img , this condition is false for any div . As a result, the filter does not exclude any div from the list. It works just like a general div selector. | All div s are selected. (Div 1, 2, 3) |
div:not(a:has(img)) | div | Logically meaningless. :not() here asks each div , "Are you yourself an a tag that has an img inside?" Since a div can never be an a tag, this condition is also false from the start. As a result, the filter does not exclude any div , and it also behaves like a general div selector. | All div s are selected. (Div 1, 2, 3) |
div:not(:has(img)) | div | Effective and meaningful. :not() here uses :has() to check the inner content of the div . It asks each div , "Is there an img tag inside you?" For the div s where the answer is "yes" (the first and second div s), :not() excludes them. Only the one for which the answer is "no" (the third div ) remains. | Only the third div is selected. |
div img:not(a > img) | img | Effective and completely different. Its main target is not the div , but the img tag inside the div . It asks each img , "Is your direct parent an a tag?" For photoOne.png the answer is "yes", so it is excluded. For photoTwo.png the answer is "no", so it gets selected. | Only the photoTwo.png image is selected. |
This comparison table shows that the functionality of the :not()
selector is meaningful only when it is written based on the element's own characteristics. On the other hand, to use :has()
, the related structure (like whether there is an img
inside) must be checked accurately.
Showing cool stuff
Here are some UI component examples as practical applications of :not()
and :has()
, which may be helpful in your future projects.
Stuff 1
Interactive Hover Cards: Focus and Blur Effect
Stuff 2
Interactive macOS-style Dock: Hover Effect on Siblings
Stuff 3
Smart To-Do List: With Dynamic Progress Bar
Key Differences Between :not()
and :has()
Feature | :not(X) | :has(X) |
Core Question | "Am I...?" | "Is there... inside/next to me?" |
Condition applies to | The selected element itself or the full selector that describes a relationship. | The descendants or subsequent siblings of the selected element. |
Main Purpose | To exclude an element from a list based on its condition. | To select an element based on the condition of its inner content. |
Mode of Action | Everything except the selected element. | Only the selected element that has it inside/next to it. |
Example | li:not(.active) - The <li> element that does not have the .active class itself. | div:has(img) - The div element that has an <img> element inside it. |
Conclusion
The :not()
and :has()
pseudo-classes have made CSS unexpectedly powerful. :not()
is effective at excluding an element based on its own characteristics, even when that characteristic is a complex relational pattern. On the other hand, :has()
helps to select a parent or a preceding element based on its inner content or adjacent siblings. Do not complicate your code by using these pseudo-classes for unnecessary tasks; if you are more comfortable with JavaScript, use it.
By keeping these rules in mind and practicing with the examples, you will no longer be confused about when and how to use these powerful CSS selectors. This will make your code more concise, clean, and maintainable, and in many cases, will reduce the need for JavaScript. May your web development journey be successful!
Subscribe to my newsletter
Read articles from Ranjan Pal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ranjan Pal
Ranjan Pal
I am a Web Developer and Web Designer, also i love write content regarding 'Tech' niche.