Advanced Selectors You Probably Never Use
Modern CSS offers a rich set of selectors that go far beyond classes, IDs, and basic descendants. Many of these advanced selectors can dramatically simplify your stylesheets, reduce the need for extra markup, and make your code more expressive and maintainable. This article explores some of the most powerful (and underused) selectors you should consider adding to your toolkit.
1. The :is() Selector: Powerful Grouping
The :is() pseudo-class lets you group multiple selectors into one, reducing repetition and improving readability. It selects any element that matches one of the selectors inside its parentheses.
/* Without :is() */
main > h2,
main > h3,
main > h4 {
margin-block: 1.5rem 0.75rem;
}
/* With :is() */
main > :is(h2, h3, h4) {
margin-block: 1.5rem 0.75rem;
}
Why it's useful:
- Reduces selector duplication dramatically.
- Makes refactoring easier (edit one place instead of many).
- Works particularly well with complex selector chains.
Tip: :is() does not increase specificity beyond its most specific inner selector, which makes it safer than older tricks for grouping complex selectors.
2. The :where() Selector: Zero-Specificity Grouping
:where() looks similar to :is() but has zero specificity, regardless of what's inside. This makes it an excellent tool for defining defaults that are easy to override.
/* Define low-specificity defaults */
:where(article, section, aside) h2 {
font-size: 1.5rem;
margin-block: 1.5rem 1rem;
}
Why it's useful:
- Allows you to create global or layout-level defaults.
- Later, more specific rules can override these without fighting specificity.
- Makes large design systems easier to scale.
Rule of thumb: Use :is() when you need normal specificity. Use :where() for defaults you expect to override often.
3. The :has() Selector: Parent-Style Based on Children
:has() is often described as a "parent selector". It matches an element if it contains (or is followed by) something matching the selector inside :has(). Browser support is now strong in modern browsers, making this a game changer.
/* Highlight cards that contain an image */
.card:has(img) {
border-color: var(--accent);
}
/* Add spacing only if a form row has an error message */
.form-row:has(.error-message) {
margin-bottom: 1.5rem;
}
/* Style a navigation item differently if it contains an active link */
.nav-item:has(a[aria-current="page"]) {
background: var(--nav-active-bg);
}
Why it's useful:
- Lets you change a parent's style based on its children, without extra classes.
- Reduces JavaScript needed for "state-based" styling.
- Enables patterns like "only show this border if there's more than one child".
/* Add divider only when a list item is followed by another item */
li:has(+ li) {
border-bottom: 1px solid #ddd;
}
Performance note: :has() can be more expensive than simple selectors, so avoid overly complex patterns inside it, especially on very large DOMs.
4. Relational Selectors: :has() with Combinators
Because :has() accepts full selectors, you can use combinators (like >, +, and ~) inside it to express rich relationships in pure CSS.
/* Style label if the following input is focused */
label:has(+ input:focus) {
color: var(--accent);
font-weight: 600;
}
/* Collapse top margin for a section if it directly follows a header */
header:has(+ section.hero) {
margin-bottom: 0;
}
These patterns replace many "add a class to the parent when X occurs" tricks previously done with JavaScript.
5. The :not() Selector: Negation with More Power Than You Think
:not() excludes elements that match its argument. When combined with :is() or attribute selectors, you can write expressive, concise filters.
/* All buttons except the primary variant */
button:not(.btn-primary) {
background: transparent;
}
/* Links that are NOT in the main navigation */
:not(nav) > a {
text-decoration: underline;
}
/* Inputs that are not disabled and not read-only */
input:not(:disabled, [readonly]) {
cursor: text;
}
Why it's useful:
- Eliminates the need for extra "inverse" classes like
.btn-secondaryor.no-underlinein some cases. - Lets you write "everyone except..." rules very clearly.
6. Attribute Selectors: Beyond [type="text"]
Attribute selectors can do more than check for exact matches. They can match prefixes, suffixes, substrings, and tokenized lists.
/* Starts with: ^= */
[class^="icon-"] {
inline-size: 1.25rem;
block-size: 1.25rem;
}
/* Ends with: $= */
[a$=".pdf"]::after {
content: " (PDF)";
}
/* Contains substring: *= */
[class*="-danger"] {
color: var(--danger);
}
/* Contains token in space-separated list: ~= */
[data-flags~="beta"] {
border-style: dashed;
}
/* Contains token in dash-separated list: |= */
[lang|="en"] {
font-variant-ligatures: common-ligatures;
}
Use cases:
- Lightweight theming based on naming conventions.
- Indicating file types, languages, or flags without extra classes.
- Styling BEM-like patterns (
block--modifier) by suffix or substring.
7. The :nth-child() Family: Patterns, Not Just Odd/Even
Most codebases use :nth-child(odd) or :nth-child(even), but :nth-child() can express much more complex patterns.
/* Every 3rd card gets a special style */
.card:nth-child(3n) {
border-color: var(--accent);
}
/* Skip the first item, then select every 2nd: 2n+2 */
.list-item:nth-child(2n + 2) {
opacity: 0.7;
}
/* Only the first 3 items */
.list-item:nth-child(-n + 3) {
font-weight: 600;
}
Related selectors:
:nth-of-type()– based on element type instead of any child.:first-child,:last-child,:only-child– more readable for simple cases.:nth-last-child()– counts from the end.
/* Make the last 2 items muted */
.list-item:nth-last-child(-n + 2) {
color: #777;
}
8. Structural Pseudo-Classes for Content-Driven Styling
Structural selectors let you depend on the document structure instead of hard-coded classes. Used carefully, they reduce markup noise.
/* Style the first paragraph in an article differently */
article > p:first-of-type {
font-size: 1.1rem;
font-weight: 500;
}
/* Remove margin after the last element in a container */
.container > *:last-child {
margin-bottom: 0;
}
/* Emphasize a single child if it is alone */
.card > :only-child {
margin: 0;
text-align: center;
}
When to use: These selectors are most powerful when your HTML has a predictable structure (for example, CMS-driven content, documentation, or blogs) and you want "smart" defaults based on that structure.
9. Pseudo-Elements: ::marker, ::selection, and More
Pseudo-elements are not new, but some of their lesser-used variants can drastically polish your UI with very little code.
::marker: Customizing List Bullets
ul.fancy-list li::marker {
color: var(--accent);
font-size: 1.2em;
}
::selection: Text Highlight Style
::selection {
background: #222;
color: #fff;
}
::file-selector-button: File Input Button
input[type="file"]::file-selector-button {
padding: 0.5rem 1rem;
border-radius: 999px;
border: none;
background: var(--accent);
color: #fff;
}
These selectors help you control the little details that usually require hacks or JavaScript, especially around form controls and typographic presentation.
10. The :root Selector for Design Tokens
:root targets the top-level element (usually <html>) and is ideal for defining CSS custom properties (variables) that are accessible throughout your stylesheet.
:root {
--font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--color-bg: #0f172a;
--color-fg: #e5e7eb;
--color-accent: #3b82f6;
}
body {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-fg);
}
While :root is widely known, pairing it with advanced selectors such as :has() and attribute selectors (for theme toggles or layout modes) unlocks even more powerful patterns.
html[data-theme="dark"] {
--color-bg: #020617;
--color-fg: #e5e7eb;
}
11. Combining Advanced Selectors for Real-World Patterns
The real strength of these selectors emerges when you combine them. Here are a few patterns that show how they work together to replace verbose HTML or JavaScript-heavy approaches.
Context-Aware Buttons with :is() and :not()
/* Any primary button inside a destructive action area becomes red */
[data-context="danger"] :is(button, .btn-primary):not([data-variant="ghost"]) {
background: var(--danger);
}
Smart Card Layouts with :has()
/* Add extra padding only if the card has a footer */
.card:has(.card-footer) {
padding-bottom: 1.5rem;
}
Responsive Grids with Structural Selectors
/* Remove right margin from cards wrapping onto a new row */
@media (min-width: 48rem) {
.card:nth-child(3n) {
margin-right: 0;
}
}
Once you understand these tools, you can express many layout and state rules directly in CSS, cleaning up both your HTML and JavaScript.
12. Practical Guidelines for Using Advanced Selectors
- Check browser support for
:has()and newer pseudo-elements, especially if you must support older browsers. - Prefer readability over clever tricks; use complex selectors only where they genuinely simplify things.
- Watch specificity, especially with
:is()vs:where(). - Avoid overmatching; broad selectors like
*:not(...)can be expensive and hard to debug. - Document your patterns so other developers know why a particular selector is written in a complex way.


