Introduction
For decades, CSS selectors have flowed in one direction: downward. You could select children of elements, siblings after other siblings, and elements based on their own attributes or state. But selecting a parent based on its children? That was impossible without JavaScript. Developers resorted to adding classes to parent elements, using JavaScript to observe child state, or restructuring their HTML to work around this fundamental limitation.
The CSS :has() selector changes everything. Often called the "parent selector," :has() allows you to select an element based on whether it contains specific descendants, siblings, or other relative elements. But calling it merely a "parent selector" undersells its power — :has() is a relational selector that enables conditional logic entirely in CSS, reducing JavaScript dependencies and enabling cleaner, more maintainable markup.
This guide covers the :has() selector from syntax fundamentals through advanced patterns that transform how you write CSS, with practical examples you can apply immediately in production.
Understanding :has(): Core Concepts
Basic Syntax
The :has() pseudo-class takes a forgiving relative selector list as its argument. It matches an element if the selector list matches at least one element when anchored at the element in question.
/* Select a card that contains an image */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
/* Select a form that contains an invalid input */
form:has(:invalid) {
border-color: #ef4444;
}
/* Select a section that contains a highlighted paragraph */
section:has(p.highlight) {
background: #fffbeb;
}Relational Selectors
:has() is not limited to descendant relationships. It can express complex relational conditions:
/* Direct child */
.card:has(> img) { }
/* Adjacent sibling */
h2:has(+ p) { margin-bottom: 0.5rem; }
/* General sibling */
h2:has(~ .code-block) { margin-bottom: 1rem; }
/* Previous sibling (using :has with +) */
p:has(+ ul) { margin-bottom: 0.25rem; }Negation with :has()
Combine :has() with :not() for powerful negative conditions:
/* Card without an image */
.card:not(:has(img)) {
grid-template-rows: 1fr;
}
/* Form with no valid inputs */
form:has(:invalid):not(:valid) {
border-color: #ef4444;
}Architecture and Design Patterns
The Conditional Layout Pattern
Use :has() to change a parent's layout based on what content it contains. A card component that adapts its grid structure depending on whether it has an image, a video, or no media at all:
.card {
display: grid;
grid-template-rows: 1fr auto;
}
.card:has(img) {
grid-template-rows: 200px 1fr auto;
}
.card:has(video) {
grid-template-rows: 300px 1fr auto;
}
.card:has(img):has(.card-badge) {
grid-template-rows: 200px 1fr auto auto;
}The Empty State Pattern
Show placeholder content when a container has no meaningful children:
.list:has(li) .empty-state {
display: none;
}
.list:not(:has(li)) .empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px;
color: #6b7280;
}The Form Validation Pattern
Style form containers and their children based on validation state without JavaScript:
.form-group:has(:invalid) label {
color: #ef4444;
}
.form-group:has(:focus) label {
color: #2563eb;
}
.form-group:has(:valid) .validation-icon {
color: #22c55e;
}
.submit-button:has(~ .form:has(:invalid)) {
opacity: 0.5;
pointer-events: none;
}The Contextual Styling Pattern
Apply different styles to elements based on the broader context they appear in:
/* Links in a nav vs links in content */
nav a:has(> img) {
display: flex;
align-items: center;
}
/* Different heading styles based on what follows */
h2:has(+ p) {
margin-bottom: 0.25rem;
}
h2:has(+ .code-block) {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}Step-by-Step Implementation
Building an Adaptive Card Component
Create a card that automatically adjusts its layout based on content:
<article class="card">
<img src="photo.jpg" alt="Photo" class="card-media">
<div class="card-body">
<span class="card-badge">New</span>
<h3 class="card-title">Article Title</h3>
<p class="card-text">Description text here...</p>
<div class="card-actions">
<button class="btn btn-primary">Read More</button>
</div>
</div>
</article>.card {
display: grid;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
/* Card with image: image on top */
.card:has(.card-media) {
grid-template-rows: 200px 1fr;
}
/* Card without image: full body area */
.card:not(:has(.card-media)) {
grid-template-rows: 1fr;
}
/* Card with badge: add extra padding at top */
.card:has(.card-badge) .card-body {
padding-top: 24px;
}
/* Card with actions: push actions to bottom */
.card:has(.card-actions) .card-body {
display: flex;
flex-direction: column;
}
.card:has(.card-actions) .card-text {
flex: 1;
}
/* Card without text: smaller padding */
.card:not(:has(.card-text)) .card-body {
padding: 12px;
}Building a Smart Form Layout
Style form elements based on their state and context:
<form class="smart-form">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" required>
<span class="help-text">Enter your email address</span>
<span class="error-text">Please enter a valid email</span>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input type="text" id="name" required minlength="2">
<span class="help-text">Your display name</span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>/* Form group focus styling */
.form-group:has(input:focus) label {
color: #2563eb;
font-weight: 600;
}
.form-group:has(input:focus) .help-text {
color: #2563eb;
}
/* Error styling */
.form-group:has(input:invalid:not(:placeholder-shown)) label {
color: #ef4444;
}
.form-group:has(input:invalid:not(:placeholder-shown)) .error-text {
display: block;
}
.form-group:has(input:invalid:not(:placeholder-shown)) input {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Valid styling */
.form-group:has(input:valid:not(:placeholder-shown)) input {
border-color: #22c55e;
}
.form-group:has(input:valid:not(:placeholder-shown)) label::after {
content: " ✓";
color: #22c55e;
}
/* Hide error text by default */
.error-text {
display: none;
color: #ef4444;
font-size: 0.875rem;
}Building a Responsive Navigation
Toggle between mobile and desktop navigation patterns:
/* When mobile menu toggle exists and is checked */
nav:has(.menu-toggle:checked) .nav-links {
display: flex;
flex-direction: column;
position: absolute;
top: 64px;
left: 0;
right: 0;
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* When user has scrolled (add via JS to body) */
body:has(.scrolled) nav {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}Real-World Use Cases
Use Case 1: Dynamic Table Rows
Style table rows based on their content. Highlight rows with warnings, dim rows that are disabled, and add visual cues based on cell values — all without JavaScript class manipulation:
tr:has(td.warning) {
background: #fffbeb;
border-left: 3px solid #f59e0b;
}
tr:has(td:empty) {
opacity: 0.5;
}
tr:has(input[type="checkbox"]:checked) {
background: #eff6ff;
}Use Case 2: Media Query Alternatives
Use :has() to create container-like responsive behavior based on content presence. A layout that reflows when images load, or a sidebar that adjusts based on widget count:
.grid:has(> :nth-child(4)) {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.grid:not(:has(> :nth-child(2))) {
grid-template-columns: 1fr;
}Use Case 3: Theme-Aware Components
Components that automatically adapt their styling based on whether they're inside a dark or light theme container:
[data-theme="dark"] .card:has(img) {
border-color: #374151;
}
[data-theme="dark"] .card:has(.card-badge) .card-badge {
background: #1e40af;
}Use Case 4: Accessibility Improvements
Automatically add visual indicators for accessibility-related elements:
label:has(+ input[aria-required="true"])::after {
content: " *";
color: #ef4444;
}
form:has(:focus-visible) .form-group {
outline: 2px solid #2563eb;
outline-offset: 2px;
}Best Practices for Production
-
Use
:has()to reduce JavaScript — Before reaching for JavaScript to toggle classes on parent elements, check if:has()can express the condition in pure CSS. Common examples include form validation styling, content-dependent layouts, and interactive state changes. -
Be specific with
:has()selectors — Broad:has()selectors likediv:has(> *)force the browser to check many elements. Narrow the scope with specific child selectors:.card:has(> img.card-media). -
Avoid deep descendant checks —
:has()with deep descendant selectors (e.g.,.container:has(div > div > div > span)) is expensive. Prefer direct child relationships (>) or adjacent siblings (+) when possible. -
Combine with CSS Grid for adaptive layouts —
:has()can changegrid-template-rowsorgrid-template-columnsbased on content, creating layouts that adapt to their content without media queries. -
Use for progressive enhancement — Wrap
:has()features in@supportsfor critical layouts where older browser support is needed:
@supports selector(:has(*)) {
.card:has(img) {
grid-template-rows: 200px 1fr;
}
}-
Prefer
:has()over JavaScript MutationObserver — For styling that depends on child presence,:has()is more performant than observing DOM mutations and toggling classes. -
Use
:has()for CSS-only interactive patterns — Checkbox and radio button hacks that relied on adjacent sibling selectors can now use:has()for more flexible DOM structures. -
Profile performance with complex selectors — While
:has()is well-optimized in modern browsers, extremely complex selectors in large DOMs can impact style recalculation. Use browser DevTools to verify performance.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
Overly broad :has() selectors | Slow style recalculation on large DOMs | Scope with specific classes and direct child selectors |
Using :has() when a simple class would work | Unnecessary complexity | Use classes for known states; use :has() for content-dependent states |
| Forgetting browser support for older projects | Broken layouts in Safari < 15.4, Firefox < 121 | Use @supports to provide fallback styles |
Circular :has() references | Selector never matches or infinite loop | Avoid .a:has(.b) and .b:has(.a) in the same rule set |
:has() with dynamic content | Layout shifts when content loads | Consider skeleton states and min-height to prevent CLS |
Performance Optimization
The :has() selector is optimized by modern browser engines, but its performance depends on how it's written. The browser evaluates :has() selectors during style resolution, checking whether the anchor element contains matching descendants. This cost scales with the breadth of the selector argument and the size of the DOM subtree.
For optimal performance, prefer simple arguments inside :has(). A single class selector (.card:has(img)) is faster than a complex descendant selector (.card:has(div > p.special)). Use direct child combinators (>) to limit the search scope.
Avoid applying :has() to elements that change frequently. If a container's children change on every scroll or animation frame, the :has() check runs each time the style is invalidated. In such cases, a JavaScript-toggled class may be more appropriate.
/* Efficient: direct class check */
.sidebar:has(.widget-collapsed) {
width: 60px;
}
/* Less efficient: deep descendant search */
.sidebar:has(div > div > span:empty) {
width: 60px;
}Comparison with Alternatives
| Feature | CSS :has() | JavaScript class toggle | MutationObserver | CSS Container Queries |
|---|---|---|---|---|
| Selects based on children | Yes | Manual | Yes (via JS) | No (queries size) |
| Pure CSS | Yes | No | No | Yes |
| Performance | Native CSS (fast) | Fast after initial | Moderate | Native CSS (fast) |
| Reactive to DOM changes | Yes | Yes (via JS) | Yes | No (size only) |
| Browser support | Modern (2023+) | All | All | Modern (2023+) |
| Complexity | Low | Medium | High | Low |
| Cascade integration | Full | Manual class management | Manual | Full |
Advanced Patterns
CSS-Only Tabs with :has()
Replace JavaScript tab logic with :has() and radio buttons:
.tabs:has(.tab-1:checked) .tab-content-1,
.tabs:has(.tab-2:checked) .tab-content-2,
.tabs:has(.tab-3:checked) .tab-content-3 {
display: block;
}
.tabs:has(.tab-1:checked) .tab-label-1,
.tabs:has(.tab-2:checked) .tab-label-2,
.tabs:has(.tab-3:checked) .tab-label-3 {
border-bottom-color: #2563eb;
color: #2563eb;
}Conditional Grid Spans
Adjust grid spans based on sibling count:
.grid:has(> :only-child) > * {
grid-column: span 2;
}
.grid:has(> :nth-child(2)):not(:has(> :nth-child(3))) > * {
grid-column: span 1;
}Hover Chain Effects
Propagate hover effects up the DOM tree:
.row:has(.cell:hover) {
background: #f0f9ff;
}
.column:has(.cell:hover) .cell {
background: #eff6ff;
}State-Based Layout Switching
/* Collapse sidebar when it has no widgets */
.layout:not(:has(.sidebar > *)) {
grid-template-columns: 1fr;
}
/* Show toolbar only when editable content is present */
.toolbar:has(~ .editor:focus-within) {
display: flex;
}Testing Strategies
Test :has() selectors with Playwright by manipulating child content and verifying parent styles:
test('card adapts when image is present', async ({ page }) => {
await page.goto('/demo/card');
// Card with image
const cardWithImage = page.locator('.card:has(img)');
await expect(cardWithImage).toHaveCSS('grid-template-rows', '200px 1fr auto');
});
test('card adapts when image is removed', async ({ page }) => {
await page.goto('/demo/card');
await page.locator('.card img').remove();
const cardWithoutImage = page.locator('.card');
await expect(cardWithoutImage).toHaveCSS('grid-template-rows', '1fr auto');
});
test('form group highlights on focus', async ({ page }) => {
await page.goto('/demo/form');
await page.locator('#email').focus();
const group = page.locator('.form-group').first();
const label = group.locator('label');
await expect(label).toHaveCSS('color', 'rgb(37, 99, 235)');
});Future Outlook
The :has() selector is part of a broader CSS evolution toward conditional styling without JavaScript. Combined with container queries, @scope, and the Popover API, CSS is becoming capable of handling interactions that previously required JavaScript. Future CSS features like @function and custom state pseudo-classes (:state()) will further reduce the need for JavaScript-driven class manipulation, making :has() a cornerstone of modern CSS architecture.
Cross-Browser Testing Strategy
Modern CSS features often have varying levels of browser support, making a systematic cross-browser testing strategy essential. Before using any CSS feature in production, verify its support status and implement appropriate fallbacks for browsers that haven't yet implemented it.
Progressive Enhancement with @supports
Use the @supports at-rule to provide fallback styles for browsers that don't support specific CSS features:
/* Base styles for all browsers */
.grid-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.grid-container > * {
flex: 1 1 300px;
}
/* Enhanced layout for browsers supporting CSS Grid */
@supports (display: grid) {
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.grid-container > * {
flex: none;
}
}
/* Further enhancement with subgrid support */
@supports (grid-template-columns: subgrid) {
.grid-container {
grid-template-columns: subgrid;
}
}Visual Regression Testing
Implement visual regression testing to catch unintended layout shifts and styling changes. Tools like Percy, Chromatic, or Playwright's screenshot comparison can detect visual differences across browsers and screen sizes:
const { test, expect } = require('@playwright/test');
test('responsive layout matches design', async ({ page }) => {
await page.goto('/components/dashboard');
// Test at multiple viewport sizes
for (const viewport of [
{ width: 375, height: 812, name: 'mobile' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 1440, height: 900, name: 'desktop' },
]) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await expect(page).toHaveScreenshot(
`dashboard-${viewport.name}.png`,
{ maxDiffPixels: 100 }
);
}
});Browser Compatibility Testing Matrix
Maintain a testing matrix that covers the browsers and versions your users actually use. Use analytics data to determine your browser support baseline, then configure tools like Browserslist to automatically handle polyfilling and prefixing:
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"not ie 11"
]
}This data-driven approach ensures you're spending testing effort where it matters most, rather than trying to support every possible browser configuration.
Community Resources and Further Learning
The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.
Curated Learning Pathways
Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.
Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.
Contributing to Open Source
Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.
# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
# Run the project's contribution setup
npm run setup:dev
npm run test # Ensure tests pass before making changes
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
Closes #1234"
git push origin fix/issue-descriptionBuilding a Technical Knowledge Base
Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.
Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.
Staying Current with Industry Trends
Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.
Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.
Mentorship and Knowledge Sharing
Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.
Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.
Conclusion
The CSS :has() selector is the most powerful addition to CSS selectors in decades. The key takeaways are:
:has()enables parent selection — select elements based on their descendants, siblings, or any relational condition.- Replace JavaScript class toggles — use
:has()for content-dependent styling like form validation, empty states, and adaptive layouts. - Prefer specific arguments — narrow the scope of
:has()checks for better performance on large DOMs. - Combine with modern CSS — pair with container queries, Grid, and custom properties for powerful, pure-CSS component architectures.
- Provide fallbacks — use
@supports selector(:has(*))for critical layouts in projects supporting older browsers.
Start by identifying places where JavaScript toggles classes on parent elements based on child state. In most cases, :has() can replace that JavaScript entirely, resulting in cleaner markup and fewer runtime dependencies.