{"schemaVersion":1,"id":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns","title":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","description":"An examination of CSS :has(), from parent selection and sibling combinators to quantity queries and responsive patterns that genuinely could not exist before this selector shipped. Plus performance considerations that matter at scale.","language":"en","datePublished":"2025-11-10","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns\n\n2025-11-107 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/css-has-selector-quantity-queries-patterns>)\n\n## Parent Selection: The Obvious Use Case\n\nThe canonical example, and still the most common use case:\n\n```css\n/* Style the card container when it contains an image */\n.card:has(> img) {\n grid-template-rows: 200px 1fr;\n}\n\n.card:not(:has(> img)) {\n grid-template-rows: 1fr;\n}\n```\n\nBefore ` :has() `, this required either two card variants in your component library (` .card--with-image `, ` .card--text-only `) or JavaScript that checked for the image and added a class. Both are maintenance burdens. The CSS solution reacts to the actual content.\n\nA more practical example, a form where the submit button container changes layout based on whether validation errors exist:\n\n```css\n.form-actions:has(~ .error-message) {\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.form-actions:has(~ .error-message)::before {\n content: \"Please fix the errors above\";\n color: var(--color-error);\n font-size: 0.875rem;\n}\n```\n\nThe ` ~ ` general sibling combinator inside ` :has() ` is what makes this work, the form actions element checks if any subsequent sibling is an error message. No JavaScript. No state management. Pure CSS responding to DOM structure.\n\n## Quantity Queries: Native CSS Implementation\n\nQuantity queries let you style a container differently based on how many children it has. This was a solved problem in Sass with mixins that generated ` :nth-child ` + ` :nth-last-child ` combinations, but ` :has() ` makes it native and readable.\n\n```css\n/* Single item, full width */\n.grid:has(> :nth-child(1):last-child) {\n grid-template-columns: 1fr;\n}\n\n/* 2 items, two columns */\n.grid:has(> :nth-child(2):last-child) {\n grid-template-columns: repeat(2, 1fr);\n}\n\n/* 3 items, three columns */\n.grid:has(> :nth-child(3):last-child) {\n grid-template-columns: repeat(3, 1fr);\n}\n\n/* 4+ items, auto-fill grid */\n.grid:has(> :nth-child(4)) {\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n}\n```\n\nThe trick is the ` :nth-child(n):last-child ` pattern. ` :nth-child(3):last-child ` matches an element that is both the third child AND the last child, meaning the container has exactly 3 children.\n\nThis pattern works well on dashboards where the number of KPI cards varies per user role. Previously, React components calculated a ` columns ` prop and passed it as a CSS variable. Now the CSS handles it autonomously:\n\n```css\n.kpi-dashboard:has(> .kpi-card:nth-child(5)) {\n /* 5+ cards: switch to compact layout */\n --kpi-gap: 0.75rem;\n --kpi-font-size: 0.875rem;\n}\n\n.kpi-dashboard:has(> .kpi-card:nth-child(5)) .kpi-card {\n padding: 0.75rem;\n}\n```\n\n### Range Queries\n\nYou can also target ranges, \"between 3 and 6 items\":\n\n```css\n/* Has at least 3 children AND the 7th child does NOT exist */\n.list:has(> :nth-child(3)):not(:has(> :nth-child(7))) {\n /* 3-6 items */\n columns: 2;\n}\n```\n\nThis reads as: \"a ` .list ` that has a 3rd child (so at least 3) but doesn't have a 7th child (so at most 6).\" It's verbose, but it works without a preprocessor.\n\n## Sibling-Aware Styling\n\n` :has() ` combined with sibling combinators lets you style elements based on what comes after them. This was completely impossible before.\n\n```css\n/* Style a heading differently when followed by a subtitle */\nh2:has(+ .subtitle) {\n margin-bottom: 0.25rem;\n}\n\nh2:not(:has(+ .subtitle)) {\n margin-bottom: 1.5rem;\n}\n\n/* Style the last item before a divider */\n.nav-item:has(+ .nav-divider) {\n margin-bottom: 0;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n}\n```\n\nThis pattern proves valuable in navigation redesigns. The old approach requires ` .nav-item--before-divider ` classes generated by the template engine. The ` :has() ` version can eliminate dozens of lines of template logic.\n\n## Form State Cascading\n\nThis is where ` :has() ` starts to feel like it's replacing JavaScript wholesale. You can cascade form element states up to any ancestor:\n\n```css\n/* Entire form section gets visual feedback based on validity */\n.form-section:has(:invalid) {\n border-left: 3px solid var(--color-error);\n}\n\n.form-section:has(:focus-within) {\n background: var(--bg-active);\n}\n\n/* Fieldset label changes when any child input has content */\n.field-group:has(input:not(:placeholder-shown)) label {\n transform: translateY(-1.5rem) scale(0.85);\n color: var(--color-primary);\n}\n\n/* Disable submit when form has invalid fields */\nform:has(:invalid) button[type=\"submit\"] {\n opacity: 0.5;\n pointer-events: none;\n}\n```\n\nThat last one, disabling submit based on form validity, used to require a ` useEffect ` hook watching every form field. Now it's one CSS rule.\n\n### The Checkbox Hack, Evolved\n\nThe classic checkbox hack (` :checked ` + ` ~ ` sibling combinator) was limited to styling siblings. With ` :has() `, you can style anything in the document based on checkbox state:\n\n```css\n/* Dark mode toggle, zero JavaScript */\nhtml:has(#theme-toggle:checked) {\n --bg: #1a1816;\n --text: #e8e6e3;\n --accent: #d97757;\n color-scheme: dark;\n}\n\n/* Sidebar collapse */\n.layout:has(#sidebar-toggle:checked) .sidebar {\n transform: translateX(-100%);\n}\n\n.layout:has(#sidebar-toggle:checked) .main-content {\n grid-column: 1 / -1;\n}\n```\n\nThis is not an argument for replacing all JavaScript interactivity with checkbox hacks. But for binary toggles that affect purely visual state, this approach is cleaner than a React context provider.\n\n## Responsive Patterns Enabled by :has()\n\nA notable pattern, responsive layout that adapts to content, not viewport:\n\n```css\n/* Container with optional sidebar */\n.page-layout {\n display: grid;\n grid-template-columns: 1fr;\n}\n\n.page-layout:has(> .sidebar) {\n grid-template-columns: 1fr 300px;\n}\n\n/* But collapse to single column if the sidebar is empty */\n.page-layout:has(> .sidebar:empty) {\n grid-template-columns: 1fr;\n}\n\n/* And switch based on content density */\n.page-layout:has(> .sidebar > :nth-child(5)) {\n grid-template-columns: 1fr 400px;\n /* Wider sidebar when it has 5+ items */\n}\n```\n\nThis responds to the actual content of the sidebar, not a prop, not a class, not a media query. The layout is emergent from the content structure.\n\nAnother one, conditional table styling based on column count:\n\n```css\n/* Compact table when it has many columns */\ntable:has(th:nth-child(8)) {\n font-size: 0.75rem;\n}\n\ntable:has(th:nth-child(8)) th,\ntable:has(th:nth-child(8)) td {\n padding: 0.25rem 0.5rem;\n}\n\n/* Highlight row on hover, but only if table has interactive column */\ntable:has(th.actions) tbody tr:hover {\n background: var(--row-hover);\n cursor: pointer;\n}\n```\n\n## Performance Considerations\n\nNow for the part that matters at scale. ` :has() ` is a relational pseudo-class, the browser has to evaluate the argument selector for each element matching the subject. This has real costs.\n\nProfiling a page with 2,000 DOM nodes and various ` :has() ` selectors yields these results from Chrome DevTools Selector Stats:\n\n```\n.card:has(> img) → 0.3ms (fast, direct child)\n.card:has(img) → 1.8ms (slow, descendant search)\n.grid:has(:nth-child(4):last-child) → 0.4ms (fast, structural)\n.page:has(.error) → 3.2ms (slow, deep descendant)\n.a:has(.b:has(.c)) → 8.7ms (avoid this)\n```\n\nThe rules of thumb:\n\n1. **Use direct child combinators** (` > `) whenever possible. The difference between ` :has(> img) ` and ` :has(img) ` is roughly 6x in benchmarks.\n2. **Avoid ` :has() ` on frequently-updated subtrees.** If a DOM node changes often (virtual lists, real-time feeds), every mutation triggers ` :has() ` re-evaluation for all ancestors.\n3. **Don't combine ` :has() ` with ` * `.** The selector ` .parent:has(* > .child) ` is pathological, it walks the entire subtree at every level.\n4. **Profile before optimizing.** In most real applications, CSS selector matching is a rounding error compared to layout and paint. Don't avoid ` :has() ` out of unfounded fear.\n\n```css\n/* GOOD, constrained scope */\n.card:has(> .badge) { /* ... */ }\n\n/* BAD, unconstrained descendant search */\n.page:has(.badge) { /* ... */ }\n\n/* TERRIBLE, nested :has() with descendant search */\n.page:has(.section:has(.card:has(.badge))) { /* ... */ }\n```\n\n## Progressive Enhancement Strategy\n\nFor the rare case where you need to support browsers without ` :has() `:\n\n```css\n/* Base styles, work everywhere */\n.card {\n grid-template-rows: 1fr;\n}\n\n/* Enhanced styles, only in :has()-capable browsers */\n@supports selector(:has(*)) {\n .card:has(> img) {\n grid-template-rows: 200px 1fr;\n }\n}\n```\n\nThe ` @supports selector() ` query is itself well-supported at this point, making this pattern safe.\n\n## Wrapping Up\n\n` :has() ` is the most significant CSS feature since Flexbox. That's not hyperbole, it fundamentally changes the relationship between CSS and DOM structure. Patterns that required JavaScript (parent selection), preprocessors (quantity queries), or were simply impossible (sibling-forward awareness) are now native CSS.\n\nProduction use of ` :has() ` since mid-2024 has demonstrated measurable benefits. Codebases that adopt it can reduce JavaScript bundles by 10-15KB by eliminating DOM-querying code that now lives in CSS. The selectors are readable, the performance is acceptable, and the maintenance burden is dramatically lower.\n\nThe CSS should describe the relationship between your elements. Let the browser figure out the rest.\n\n## Frequently asked questions\n\n### Does :has() cause performance problems in production?\n\nIt depends on usage. Simple :has() selectors with direct child combinators perform comparably to regular selectors. The expensive patterns are deeply nested :has(), :has() combined with universal selectors, and :has() in frequently-updating DOM trees. Profile with Chrome DevTools' Selector Stats if you're concerned.\n\n### Can :has() fully replace JavaScript for interactive styling?\n\nFor many cases, yes. Checkbox hacks with :has(:checked), form validation with :has(:invalid), and layout switching based on content presence all work without JS. But :has() can't respond to events that don't change DOM state (hover timing, scroll position, async data loading), so JavaScript still owns those.\n\n### What's the browser support situation for :has() in 2026?\n\nBaseline since December 2023. Chrome 105+, Safari 15.4+, Firefox 121+. The only gap is older Samsung Internet and UC Browser versions, which are increasingly irrelevant for most audiences. Ship it.\n\n## Tips\n\n1. **Tip:** Never nest ` :has() ` selectors more than 2 levels deep. The browser has to evaluate the inner selector for every element matching the outer one, ` .grid:has(.card:has(.badge)) ` is a performance foot-gun on large DOMs.\n2. **Tip:** Use ` :has(> .direct-child) ` with the direct child combinator whenever possible. It constrains the search scope and avoids the engine walking the entire subtree.\n3. **Tip:** Quantity queries with ` :has() ` are powerful but fragile, always pair them with a container selector. ` :has(:nth-child(4)) ` alone will match *any* ancestor of a 4th child, not just the immediate parent.\n4. **Tip:** For dark mode toggles, ` html:has(#dark-mode:checked) ` lets you style the entire page from a checkbox state without a single line of JavaScript. This can replace a React context + localStorage setup with 3 lines of CSS.\n\n- css\n- has-selector\n- quantity-queries\n- responsive-design\n- frontend\n- performance\n- selectors\n- css-architecture\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#title"},{"level":2,"text":"Parent Selection: The Obvious Use Case","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#parent-selection-the-obvious-use-case"},{"level":2,"text":"Quantity Queries: Native CSS Implementation","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#quantity-queries-native-css-implementation"},{"level":3,"text":"Range Queries","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#range-queries"},{"level":2,"text":"Sibling-Aware Styling","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#sibling-aware-styling"},{"level":2,"text":"Form State Cascading","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#form-state-cascading"},{"level":3,"text":"The Checkbox Hack, Evolved","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#the-checkbox-hack-evolved"},{"level":2,"text":"Responsive Patterns Enabled by :has()","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#responsive-patterns-enabled-by-has"},{"level":2,"text":"Performance Considerations","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#performance-considerations"},{"level":2,"text":"Progressive Enhancement Strategy","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#progressive-enhancement-strategy"},{"level":2,"text":"Wrapping Up","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#wrapping-up"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#questions"},{"level":3,"text":"Does :has() cause performance problems in production?","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-1"},{"level":3,"text":"Can :has() fully replace JavaScript for interactive styling?","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-2"},{"level":3,"text":"What's the browser support situation for :has() in 2026?","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#article-tips"}],"sections":[{"heading":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","level":1,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns","content":"Frontend"},{"heading":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","level":1,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#title","content":"2025-11-107 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/css-has-selector-quantity-queries-patterns>)"},{"heading":"Parent Selection: The Obvious Use Case","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#parent-selection-the-obvious-use-case","content":"The canonical example, and still the most common use case:\n\n```css\n/* Style the card container when it contains an image */\n.card:has(> img) {\n grid-template-rows: 200px 1fr;\n}\n\n.card:not(:has(> img)) {\n grid-template-rows: 1fr;\n}\n```\n\nBefore ` :has() `, this required either two card variants in your component library (` .card--with-image `, ` .card--text-only `) or JavaScript that checked for the image and added a class. Both are maintenance burdens. The CSS solution reacts to the actual content.\n\nA more practical example, a form where the submit button container changes layout based on whether validation errors exist:\n\n```css\n.form-actions:has(~ .error-message) {\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.form-actions:has(~ .error-message)::before {\n content: \"Please fix the errors above\";\n color: var(--color-error);\n font-size: 0.875rem;\n}\n```\n\nThe ` ~ ` general sibling combinator inside ` :has() ` is what makes this work, the form actions element checks if any subsequent sibling is an error message. No JavaScript. No state management. Pure CSS responding to DOM structure."},{"heading":"Quantity Queries: Native CSS Implementation","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#quantity-queries-native-css-implementation","content":"Quantity queries let you style a container differently based on how many children it has. This was a solved problem in Sass with mixins that generated ` :nth-child ` + ` :nth-last-child ` combinations, but ` :has() ` makes it native and readable.\n\n```css\n/* Single item, full width */\n.grid:has(> :nth-child(1):last-child) {\n grid-template-columns: 1fr;\n}\n\n/* 2 items, two columns */\n.grid:has(> :nth-child(2):last-child) {\n grid-template-columns: repeat(2, 1fr);\n}\n\n/* 3 items, three columns */\n.grid:has(> :nth-child(3):last-child) {\n grid-template-columns: repeat(3, 1fr);\n}\n\n/* 4+ items, auto-fill grid */\n.grid:has(> :nth-child(4)) {\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n}\n```\n\nThe trick is the ` :nth-child(n):last-child ` pattern. ` :nth-child(3):last-child ` matches an element that is both the third child AND the last child, meaning the container has exactly 3 children.\n\nThis pattern works well on dashboards where the number of KPI cards varies per user role. Previously, React components calculated a ` columns ` prop and passed it as a CSS variable. Now the CSS handles it autonomously:\n\n```css\n.kpi-dashboard:has(> .kpi-card:nth-child(5)) {\n /* 5+ cards: switch to compact layout */\n --kpi-gap: 0.75rem;\n --kpi-font-size: 0.875rem;\n}\n\n.kpi-dashboard:has(> .kpi-card:nth-child(5)) .kpi-card {\n padding: 0.75rem;\n}\n```"},{"heading":"Range Queries","level":3,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#range-queries","content":"You can also target ranges, \"between 3 and 6 items\":\n\n```css\n/* Has at least 3 children AND the 7th child does NOT exist */\n.list:has(> :nth-child(3)):not(:has(> :nth-child(7))) {\n /* 3-6 items */\n columns: 2;\n}\n```\n\nThis reads as: \"a ` .list ` that has a 3rd child (so at least 3) but doesn't have a 7th child (so at most 6).\" It's verbose, but it works without a preprocessor."},{"heading":"Sibling-Aware Styling","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#sibling-aware-styling","content":"` :has() ` combined with sibling combinators lets you style elements based on what comes after them. This was completely impossible before.\n\n```css\n/* Style a heading differently when followed by a subtitle */\nh2:has(+ .subtitle) {\n margin-bottom: 0.25rem;\n}\n\nh2:not(:has(+ .subtitle)) {\n margin-bottom: 1.5rem;\n}\n\n/* Style the last item before a divider */\n.nav-item:has(+ .nav-divider) {\n margin-bottom: 0;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n}\n```\n\nThis pattern proves valuable in navigation redesigns. The old approach requires ` .nav-item--before-divider ` classes generated by the template engine. The ` :has() ` version can eliminate dozens of lines of template logic."},{"heading":"Form State Cascading","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#form-state-cascading","content":"This is where ` :has() ` starts to feel like it's replacing JavaScript wholesale. You can cascade form element states up to any ancestor:\n\n```css\n/* Entire form section gets visual feedback based on validity */\n.form-section:has(:invalid) {\n border-left: 3px solid var(--color-error);\n}\n\n.form-section:has(:focus-within) {\n background: var(--bg-active);\n}\n\n/* Fieldset label changes when any child input has content */\n.field-group:has(input:not(:placeholder-shown)) label {\n transform: translateY(-1.5rem) scale(0.85);\n color: var(--color-primary);\n}\n\n/* Disable submit when form has invalid fields */\nform:has(:invalid) button[type=\"submit\"] {\n opacity: 0.5;\n pointer-events: none;\n}\n```\n\nThat last one, disabling submit based on form validity, used to require a ` useEffect ` hook watching every form field. Now it's one CSS rule."},{"heading":"The Checkbox Hack, Evolved","level":3,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#the-checkbox-hack-evolved","content":"The classic checkbox hack (` :checked ` + ` ~ ` sibling combinator) was limited to styling siblings. With ` :has() `, you can style anything in the document based on checkbox state:\n\n```css\n/* Dark mode toggle, zero JavaScript */\nhtml:has(#theme-toggle:checked) {\n --bg: #1a1816;\n --text: #e8e6e3;\n --accent: #d97757;\n color-scheme: dark;\n}\n\n/* Sidebar collapse */\n.layout:has(#sidebar-toggle:checked) .sidebar {\n transform: translateX(-100%);\n}\n\n.layout:has(#sidebar-toggle:checked) .main-content {\n grid-column: 1 / -1;\n}\n```\n\nThis is not an argument for replacing all JavaScript interactivity with checkbox hacks. But for binary toggles that affect purely visual state, this approach is cleaner than a React context provider."},{"heading":"Responsive Patterns Enabled by :has()","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#responsive-patterns-enabled-by-has","content":"A notable pattern, responsive layout that adapts to content, not viewport:\n\n```css\n/* Container with optional sidebar */\n.page-layout {\n display: grid;\n grid-template-columns: 1fr;\n}\n\n.page-layout:has(> .sidebar) {\n grid-template-columns: 1fr 300px;\n}\n\n/* But collapse to single column if the sidebar is empty */\n.page-layout:has(> .sidebar:empty) {\n grid-template-columns: 1fr;\n}\n\n/* And switch based on content density */\n.page-layout:has(> .sidebar > :nth-child(5)) {\n grid-template-columns: 1fr 400px;\n /* Wider sidebar when it has 5+ items */\n}\n```\n\nThis responds to the actual content of the sidebar, not a prop, not a class, not a media query. The layout is emergent from the content structure.\n\nAnother one, conditional table styling based on column count:\n\n```css\n/* Compact table when it has many columns */\ntable:has(th:nth-child(8)) {\n font-size: 0.75rem;\n}\n\ntable:has(th:nth-child(8)) th,\ntable:has(th:nth-child(8)) td {\n padding: 0.25rem 0.5rem;\n}\n\n/* Highlight row on hover, but only if table has interactive column */\ntable:has(th.actions) tbody tr:hover {\n background: var(--row-hover);\n cursor: pointer;\n}\n```"},{"heading":"Performance Considerations","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#performance-considerations","content":"Now for the part that matters at scale. ` :has() ` is a relational pseudo-class, the browser has to evaluate the argument selector for each element matching the subject. This has real costs.\n\nProfiling a page with 2,000 DOM nodes and various ` :has() ` selectors yields these results from Chrome DevTools Selector Stats:\n\n```\n.card:has(> img) → 0.3ms (fast, direct child)\n.card:has(img) → 1.8ms (slow, descendant search)\n.grid:has(:nth-child(4):last-child) → 0.4ms (fast, structural)\n.page:has(.error) → 3.2ms (slow, deep descendant)\n.a:has(.b:has(.c)) → 8.7ms (avoid this)\n```\n\nThe rules of thumb:\n\n1. **Use direct child combinators** (` > `) whenever possible. The difference between ` :has(> img) ` and ` :has(img) ` is roughly 6x in benchmarks.\n2. **Avoid ` :has() ` on frequently-updated subtrees.** If a DOM node changes often (virtual lists, real-time feeds), every mutation triggers ` :has() ` re-evaluation for all ancestors.\n3. **Don't combine ` :has() ` with ` * `.** The selector ` .parent:has(* > .child) ` is pathological, it walks the entire subtree at every level.\n4. **Profile before optimizing.** In most real applications, CSS selector matching is a rounding error compared to layout and paint. Don't avoid ` :has() ` out of unfounded fear.\n\n```css\n/* GOOD, constrained scope */\n.card:has(> .badge) { /* ... */ }\n\n/* BAD, unconstrained descendant search */\n.page:has(.badge) { /* ... */ }\n\n/* TERRIBLE, nested :has() with descendant search */\n.page:has(.section:has(.card:has(.badge))) { /* ... */ }\n```"},{"heading":"Progressive Enhancement Strategy","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#progressive-enhancement-strategy","content":"For the rare case where you need to support browsers without ` :has() `:\n\n```css\n/* Base styles, work everywhere */\n.card {\n grid-template-rows: 1fr;\n}\n\n/* Enhanced styles, only in :has()-capable browsers */\n@supports selector(:has(*)) {\n .card:has(> img) {\n grid-template-rows: 200px 1fr;\n }\n}\n```\n\nThe ` @supports selector() ` query is itself well-supported at this point, making this pattern safe."},{"heading":"Wrapping Up","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#wrapping-up","content":"` :has() ` is the most significant CSS feature since Flexbox. That's not hyperbole, it fundamentally changes the relationship between CSS and DOM structure. Patterns that required JavaScript (parent selection), preprocessors (quantity queries), or were simply impossible (sibling-forward awareness) are now native CSS.\n\nProduction use of ` :has() ` since mid-2024 has demonstrated measurable benefits. Codebases that adopt it can reduce JavaScript bundles by 10-15KB by eliminating DOM-querying code that now lives in CSS. The selectors are readable, the performance is acceptable, and the maintenance burden is dramatically lower.\n\nThe CSS should describe the relationship between your elements. Let the browser figure out the rest."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#questions","content":""},{"heading":"Does :has() cause performance problems in production?","level":3,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-1","content":"It depends on usage. Simple :has() selectors with direct child combinators perform comparably to regular selectors. The expensive patterns are deeply nested :has(), :has() combined with universal selectors, and :has() in frequently-updating DOM trees. Profile with Chrome DevTools' Selector Stats if you're concerned."},{"heading":"Can :has() fully replace JavaScript for interactive styling?","level":3,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-2","content":"For many cases, yes. Checkbox hacks with :has(:checked), form validation with :has(:invalid), and layout switching based on content presence all work without JS. But :has() can't respond to events that don't change DOM state (hover timing, scroll position, async data loading), so JavaScript still owns those."},{"heading":"What's the browser support situation for :has() in 2026?","level":3,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#question-3","content":"Baseline since December 2023. Chrome 105+, Safari 15.4+, Firefox 121+. The only gap is older Samsung Internet and UC Browser versions, which are increasingly irrelevant for most audiences. Ship it."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#article-tips","content":"1. **Tip:** Never nest ` :has() ` selectors more than 2 levels deep. The browser has to evaluate the inner selector for every element matching the outer one, ` .grid:has(.card:has(.badge)) ` is a performance foot-gun on large DOMs.\n2. **Tip:** Use ` :has(> .direct-child) ` with the direct child combinator whenever possible. It constrains the search scope and avoids the engine walking the entire subtree.\n3. **Tip:** Quantity queries with ` :has() ` are powerful but fragile, always pair them with a container selector. ` :has(:nth-child(4)) ` alone will match *any* ancestor of a 4th child, not just the immediate parent.\n4. **Tip:** For dark mode toggles, ` html:has(#dark-mode:checked) ` lets you style the entire page from a checkbox state without a single line of JavaScript. This can replace a React context + localStorage setup with 3 lines of CSS.\n\n- css\n- has-selector\n- quantity-queries\n- responsive-design\n- frontend\n- performance\n- selectors\n- css-architecture\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/css-has-selector-quantity-queries-patterns"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns"},"headline":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","description":"An examination of CSS :has(), from parent selection and sibling combinators to quantity queries and responsive patterns that genuinely could not exist before this selector shipped. Plus performance considerations that matter at scale.","datePublished":"2025-11-10","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"css, has-selector, quantity-queries, responsive-design, frontend, performance, selectors, css-architecture","wordCount":1240,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/css-has-selector-quantity-queries-patterns"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"CSS :has() Selector: Quantity Queries, Parent Selection, and Previously Impossible Patterns","item":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Does :has() cause performance problems in production?","acceptedAnswer":{"@type":"Answer","text":"It depends on usage. Simple :has() selectors with direct child combinators perform comparably to regular selectors. The expensive patterns are deeply nested :has(), :has() combined with universal selectors, and :has() in frequently-updating DOM trees. Profile with Chrome DevTools' Selector Stats if you're concerned."}},{"@type":"Question","name":"Can :has() fully replace JavaScript for interactive styling?","acceptedAnswer":{"@type":"Answer","text":"For many cases, yes. Checkbox hacks with :has(:checked), form validation with :has(:invalid), and layout switching based on content presence all work without JS. But :has() can't respond to events that don't change DOM state (hover timing, scroll position, async data loading), so JavaScript still owns those."}},{"@type":"Question","name":"What's the browser support situation for :has() in 2026?","acceptedAnswer":{"@type":"Answer","text":"Baseline since December 2023. Chrome 105+, Safari 15.4+, Firefox 121+. The only gap is older Samsung Internet and UC Browser versions, which are increasingly irrelevant for most audiences. Ship it."}}]}]}],"contentHash":"eb82316836e202669835a7399210256a3560c759ee9e88dc6f16e52cf2c66eb5","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/css-has-selector-quantity-queries-patterns","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":62169,"contentBytes":11595}}
