{"id":45,"date":"2026-10-03T07:57:42","date_gmt":"2026-10-03T07:57:42","guid":{"rendered":"https:\/\/webdevpuneet.com\/blog\/?p=45"},"modified":"2026-10-03T07:57:46","modified_gmt":"2026-10-03T07:57:46","slug":"css-has-selector-10-practical-examples-you-can-click-and-edit","status":"publish","type":"post","link":"https:\/\/webdevpuneet.com\/blog\/css-has-selector-10-practical-examples-you-can-click-and-edit\/","title":{"rendered":"CSS :has() Selector: 10 Practical Examples You Can Click and Edit"},"content":{"rendered":"<p><!--\n================================================================\n  POST TITLE (paste into the WordPress title field \u2014 it becomes the page H1)\n  CSS :has() Selector: 10 Practical Examples You Can Click and Edit\n\n  SEO TITLE (50 chars, target 50\u201360, max 60 \u2014 \"Optimize SEO\" panel \u2192 SEO TITLE)\n  CSS :has() Selector: 10 Live Examples You Can Edit\n\n  SEO DESCRIPTION (159 chars, target 150\u2013160, max 160 \u2014 \"Optimize SEO\" panel \u2192 SEO DESCRIPTION)\n  Learn the CSS :has() parent selector with 10 clickable live demos: cards, forms, tables, menus and more. Edit each one free in your browser, no sign-up needed.\n\n  HOW TO PASTE: click the first line after this comment, press Ctrl+Shift+End, Ctrl+C.\n  In WordPress click \"Type \/ to choose a block\", then Ctrl+V.\n  Fallback: \u22ee menu \u2192 Code editor \u2192 paste.\n================================================================\n--><\/p>\n\n\n<p class=\"wp-block-paragraph\"><code>:has()<\/code> is the CSS feature people spent a decade calling impossible: a <strong>parent selector<\/strong>. Before it, CSS could style a descendant based on its ancestor, but never the other way round \u2014 there was no way to say &#8220;style this <code>&lt;div&gt;<\/code> differently because a specific child is present inside it.&#8221; <code>:has()<\/code> closes that gap, and it does more than the &#8220;parent selector&#8221; nickname suggests: it&#8217;s a general relational selector that can look at children, siblings, and combinations of conditions, then style the element it&#8217;s attached to based on what it finds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The 10 demos below are all real, clickable UI patterns \u2014 a selectable card, a validated form field, a table row that reacts to its own status cell, a CSS-only dropdown menu. Nothing here is decorative; every one is copy-paste-ready for an actual project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every embed also has a <strong>Fork &amp; Edit<\/strong> button that opens the exact snippet, already running, in <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> \u2014 webdevpuneet&#8217;s free in-browser editor. Rename a class, delete a line from the selector, and watch the rule stop matching. That&#8217;s a faster way to understand <em>why<\/em> a selector works than reading about it: change one thing, see what breaks, put it back.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The syntax, in one breath<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>selector:has(relative-selector) { \/* styles for selector *\/ }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>:has()<\/code> takes a selector (or a comma-separated list of them) and matches the element it&#8217;s attached to <em>if<\/em> that inner selector finds something relative to it \u2014 a descendant by default, or a sibling if you give it a combinator. It never styles the thing found inside the parentheses; it only ever decides whether the outer element matches. In demo #1 below, <code>.planCard:has(input:checked)<\/code> reads as &#8220;any <code>.planCard<\/code> that has a checked input somewhere inside it&#8221; \u2014 and the border, background, and checkmark that then get styled all belong to <code>.planCard<\/code>, never to the input itself.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why every demo here is something you click, not drag<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These demos aren&#8217;t draggable sliders, because <code>:has()<\/code> doesn&#8217;t respond to size \u2014 it responds to the DOM: a checkbox getting checked, an input becoming invalid, a child element being added or removed, a sibling being hovered. So each demo below is wired to whatever interaction actually triggers its rule: click a card, type in a field, hover a step, add a tile.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One thing worth knowing about how these are built: the JavaScript in every demo only ever <em>reports<\/em> what&#8217;s happening into the small readout under each one \u2014 it counts checked boxes, reads a field&#8217;s validity, counts child elements. It never sets the class or style that produces the visual effect. That part is left entirely to the <code>:has()<\/code> rule shown in the code tag below each demo, so what you&#8217;re watching react is genuinely the CSS, not a script pretending to be one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Selectable Plan Card<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click a pricing card and the whole card \u2014 border, background, checkmark \u2014 reacts to its own radio input being checked, no JavaScript required for the styling.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/1.html\" style=\"width:100%;height:380px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Selectable Plan Card \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.planCard:has(input:checked) {\n  border-color: var(--accent);\n  background: #eef0ff;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Before <code>:has()<\/code>, doing this meant either JavaScript toggling a class on click, or the &#8220;checkbox hack&#8221; \u2014 styling a <em>sibling<\/em> of a checked input with the <code>~<\/code> combinator, which only works if the thing you want to style happens to come after the input in the markup. Here the input is nested <em>inside<\/em> the card, and the whole card reacts \u2014 something <code>~<\/code> alone could never do.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> plan selectors, radio-card pickers, any UI where the whole card should look &#8220;selected,&#8221; not just a small radio dot in the corner. <strong>Tip:<\/strong> because this is a native input under the hood, keyboard navigation (Tab, arrow keys, Space) works automatically \u2014 you get accessible selection for free, as long as you hide the radio visually instead of using <code>display: none<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Live Invalid-Field Highlight<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Type an email address into the field \u2014 its wrapper turns red the moment the input becomes invalid, and green once it&#8217;s a properly formatted address. No submit button, no JavaScript validation logic.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/2.html\" style=\"width:100%;height:440px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Live Invalid-Field Highlight \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.field:has(input:not(:placeholder-shown):invalid) {\n  border-color: #dc2626;\n  background: #fef2f2;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>:invalid<\/code> alone would flag this field red the instant the page loads, since an empty <code>required<\/code> field is invalid by definition \u2014 that would tell a visitor they&#8217;ve made a mistake before they&#8217;ve typed a single character. Chaining in <code>:not(:placeholder-shown)<\/code> withholds the red state until there&#8217;s actually something in the field to be wrong (this relies on the input having a <code>placeholder<\/code> attribute, even a single space). <code>:has()<\/code> is what lets that whole condition style the <em>wrapper<\/em> \u2014 the label and helper text included, not just the input&#8217;s own border.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any form where you want validation feedback to feel immediate rather than waiting for a submit-and-scroll-to-the-error round trip. <strong>Tip:<\/strong> this is real, spec-defined <code>:invalid<\/code> matching \u2014 it respects whatever <code>type<\/code>, <code>pattern<\/code>, or <code>required<\/code> attributes are already on the input, so there&#8217;s no separate validation logic to keep in sync.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. &#8220;Contains a Badge&#8221; List Row<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click a notification to mark it read \u2014 its &#8220;NEW&#8221; badge is removed from the DOM, and the row&#8217;s bold styling and tinted background disappear because it no longer contains one.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/3.html\" style=\"width:100%;height:430px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Contains a Badge List Row \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.notifRow:has(.badge) {\n  background: #eef0ff;\n  font-weight: 700;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is <code>:has()<\/code> in its purest form \u2014 not reacting to a state like <code>:checked<\/code> or <code>:hover<\/code>, just to whether a particular element is present at all. If your badge is added or removed by a real change in your data (an API response, a state update in a framework), this rule needs zero JavaScript to stay in sync: the moment the badge element exists or doesn&#8217;t, the row&#8217;s styling follows.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> notification lists, inboxes, any row-based UI where an icon, badge, or flag buried inside a row should also change how the whole row reads at a glance. <strong>Tip:<\/strong> this same pattern works for &#8220;contains an error icon,&#8221; &#8220;contains an attachment,&#8221; or &#8220;contains a pinned marker&#8221; \u2014 anywhere a small piece of content should promote itself to a whole-row treatment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Quantity-Aware Grid (&#8220;Quantity Query&#8221;)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Add tiles one at a time \u2014 once the grid holds 6 or more, it switches itself to a denser 3-column layout. Remove tiles back below 6 and it reverts. No JavaScript sets that layout; the CSS does the counting.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/4.html\" style=\"width:100%;height:380px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Quantity-Aware Grid Quantity Query \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.qGrid:has(&gt; .qTile:nth-child(6)) {\n  grid-template-columns: repeat(3, 1fr);\n  gap: 6px;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is the trick known as a <strong>&#8220;quantity query&#8221;<\/strong>: <code>:nth-child(6)<\/code> matches a 6th child if one exists, so <code>:has(&gt; .qTile:nth-child(6))<\/code> reads as &#8220;has a 6th tile child&#8221; \u2014 which is only true once there are at least six. The <code>&gt;<\/code> restricts the check to direct children, so a tile nested somewhere deeper wouldn&#8217;t accidentally count. Change <code>nth-child(6)<\/code> to <code>nth-child(4)<\/code> in the editor and the switch happens a lot sooner \u2014 a good first thing to try.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> product grids, image galleries, dashboard tiles \u2014 any layout where a handful of items should get generous spacing but a full grid should tighten up rather than requiring an endless scroll. <strong>Tip:<\/strong> there&#8217;s no separate &#8220;at least N&#8221; selector \u2014 the existence of an Nth child <em>is<\/em> the &#8220;at least N&#8221; check, so for &#8220;5 or more&#8221; you&#8217;d simply use <code>:nth-child(5)<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Gated Submit Button<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The submit button looks disabled, and ignores mouse and touch clicks, until the terms checkbox is checked \u2014 driven entirely by <code>:has()<\/code> reaching from the form down to the button.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/5.html\" style=\"width:100%;height:400px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Gated Submit Button \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.gateForm:has(#terms:not(:checked)) .submitBtn {\n  opacity: .45;\n  pointer-events: none;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Notice the shape here: the condition is checked on the <em>form<\/em> (does it have an unchecked <code>#terms<\/code>?), and the styling lands on a completely different element, the button. <code>:has()<\/code> is what makes the form a legal place to ask that question at all \u2014 without it, you&#8217;d need JavaScript watching the checkbox and toggling a <code>disabled<\/code> attribute on the button by hand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> terms-of-service gates, &#8220;I&#8217;ve reviewed this&#8221; confirmations, any consent checkbox that should visibly and functionally block a next step. <strong>Tip:<\/strong> <code>pointer-events: none<\/code> is what stops mouse and touch clicks \u2014 opacity alone would still let them through. It does not stop keyboard users, though: Tab can still reach the button and Enter can still submit the form. If the gate has to be airtight, also set the <code>disabled<\/code> attribute (or validate on submit).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Status-Highlighted Table Row<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click &#8220;Cycle status&#8221; on any row \u2014 the whole row&#8217;s background reacts to whichever status cell it currently contains, cycling between on-track, overdue, and failed.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/6.html\" style=\"width:100%;height:500px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Status-Highlighted Table Row \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tr:has(td.status-overdue) { background: #fef2f2; }\ntr:has(td.status-error)   { background: #fff7ed; }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tables are a place this pattern earns its keep immediately: a status, a severity level, or a flag usually lives in one column&#8217;s cell, but a reader scanning the table benefits from the <em>whole row<\/em> carrying that signal. Before <code>:has()<\/code>, this meant a server or a script adding a class like <code>row-overdue<\/code> to the <code>&lt;tr&gt;<\/code> itself, duplicating information that was already sitting one cell over.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> admin dashboards, order lists, any tabular data where a status column should be scannable from across the room, not just readable up close. <strong>Tip:<\/strong> list your status rules in priority order \u2014 if a row could match more than one (say, both &#8220;overdue&#8221; and &#8220;error&#8221;), whichever <code>:has()<\/code> rule is defined later in the stylesheet wins under equal specificity.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Step Progress Hover Preview<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hover any step in the tracker \u2014 it and every step <em>before<\/em> it light up as a preview. Selecting a &#8220;previous sibling&#8221; like this was flatly impossible in CSS before <code>:has()<\/code> existed.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/7.html\" style=\"width:100%;height:380px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Step Progress Hover Preview \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.step:has(~ .step:hover),\n.step:hover {\n  background: var(--accent);\n  color: #fff;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is the demo that best shows off what&#8217;s genuinely new here. <code>~<\/code> is the general sibling combinator, and it has always only been able to look <em>forward<\/em> \u2014 &#8220;a <code>.step<\/code> that is followed by a hovered <code>.step<\/code>&#8221; was writable for two decades. But it could only style the <em>later<\/em> element. <code>:has(~ .step:hover)<\/code> flips the direction: it asks each step &#8220;do any of my later siblings currently have <code>:hover<\/code>?&#8221; \u2014 and answers that question about <em>itself<\/em>, which is exactly what makes the earlier steps light up. This exact &#8220;select the previous sibling&#8221; gap is the single most-requested CSS feature <code>:has()<\/code> resolved.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> checkout\/order-status trackers, multi-step wizards, breadcrumb-style progress bars where hovering a later step should preview &#8220;everything up to here.&#8221; <strong>Tip:<\/strong> this pattern works identically for a star rating widget \u2014 swap the steps for star icons and <code>:hover<\/code> for a mix of <code>:hover<\/code> and <code>:checked<\/code> radios.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. CSS-Only Dropdown Menu<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click the menu button \u2014 no JavaScript opens this. A hidden checkbox toggles, and <code>:has()<\/code> lets the wrapper react to it no matter how deeply the menu itself is nested inside.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/8.html\" style=\"width:100%;height:320px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"CSS-Only Dropdown Menu \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.ddWrap:has(#ddToggle:checked) .ddMenu {\n  display: block;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The classic &#8220;checkbox hack&#8221; for a no-JS dropdown relies on the sibling combinator too, which quietly constrains your markup: the toggle and the thing it controls have to be siblings, or the trick breaks. <code>:has()<\/code> removes that constraint \u2014 the wrapper is checking &#8220;do I contain a checked <code>#ddToggle<\/code> anywhere,&#8221; so the checkbox, the button, and the menu can be nested however your markup actually needs them to be.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> lightweight menus, filter panels, or any toggle-driven UI in a static site or email-safe context where you&#8217;d rather not reach for JavaScript at all. <strong>Tip:<\/strong> real apps almost always still want the JavaScript too \u2014 for closing the menu on an outside click, which is what this demo&#8217;s own JS does (it only ever unchecks the box; the CSS still decides what that means visually), and for closing it on <code>Escape<\/code>, which this demo doesn&#8217;t handle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. Empty-State Message<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Clear every task \u2014 the &#8220;you&#8217;re all caught up&#8221; message appears the instant the list has no items left, and disappears the moment you add one back.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/9.html\" style=\"width:100%;height:420px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Empty-State Message \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.taskList:not(:has(li)) + .emptyMsg {\n  display: block;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>:not(:has(li))<\/code> matches a list with zero matching <code>&lt;li&gt;<\/code> elements \u2014 which is a more reliable &#8220;is this empty&#8221; check than the <code>:empty<\/code> pseudo-class, since <code>:empty<\/code> also gets defeated by so much as a stray whitespace text node between tags (a near-guarantee in real, human-formatted HTML). Notice this uses a plain <code>+<\/code> adjacent-sibling combinator to reach the message, rather than a second <code>:has()<\/code> wrapped around the first \u2014 see the pitfall below for why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> to-do lists, search results, inbox views, cart pages \u2014 anywhere &#8220;nothing here&#8221; deserves an intentional message instead of a blank space that reads like the page is broken. <strong>Tip:<\/strong> pair this with a matching &#8220;results: N&#8221; counter elsewhere on the page (as this demo&#8217;s readout does), so the empty state and the count never have separate logic to fall out of sync.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">10. Favorite Toggle Gallery Card<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click the heart on any card \u2014 unlike the plan-card demo, these are independent checkboxes rather than a mutually exclusive radio group, so any number of cards can be favorited at once.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/02\/10.html\" style=\"width:100%;height:420px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Favorite Toggle Gallery Card \u2014 CSS :has() Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.galleryCard:has(.likeBox:checked) {\n  border-color: gold;\n  box-shadow: 0 0 0 3px #fef9c3;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Structurally this is the same shape as demo #1 \u2014 <code>:has(input:checked)<\/code> \u2014 but the <em>meaning<\/em> is different because the input type is different. Radios enforce mutual exclusion for free; checkboxes don&#8217;t, so this same one-line pattern quietly becomes a multi-select favorites system instead of a single-select picker, with no extra CSS or JS needed to manage the &#8220;only one at a time&#8221; logic, because there isn&#8217;t any.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> image galleries, product grids, bookmarking UI \u2014 anywhere &#8220;liked&#8221; or &#8220;saved&#8221; needs to be an independent per-item toggle rather than a single choice. <strong>Tip:<\/strong> the readout under this demo counts checked boxes with <code>document.querySelectorAll('.likeBox:checked').length<\/code> \u2014 a good reminder that even though the visual state is pure CSS, reading that state back out for a &#8220;3 favorited&#8221; counter is a completely normal, honest use of a few lines of JavaScript.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common :has() pitfalls<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>:has() cannot contain another :has() \u2014 even through a :not().<\/strong> The CSS spec explicitly disallows nesting: the selector inside <code>:has()<\/code> is not allowed to contain a second <code>:has()<\/code> anywhere within it, including tucked inside a <code>:not()<\/code>. <code>.wrap:has(.list:not(:has(li)))<\/code> is invalid CSS and the whole rule is silently ignored \u2014 no console error, it just never matches. Demo #9 avoids it by dropping the outer <code>:has()<\/code> and reaching the sibling with a plain <code>+<\/code> combinator instead.<\/li>\n\n\n\n<li><strong>:has() isn&#8217;t specificity-free \u2014 and it can end up more specific than it looks.<\/strong> The selector inside the parentheses counts toward the specificity of the whole rule, in full: <code>tr:has(td.status-overdue)<\/code> is actually <em>more<\/em> specific than a simpler-looking <code>tr.status-overdue<\/code>, because the <code>td<\/code> type selector inside <code>:has()<\/code> adds its own weight on top of the class \u2014 it beats it even when the plain class rule is declared later, which normally would be enough to win a tie. If a <code>:has()<\/code> rule refuses to lose to something you expected to override it, this is usually why.<\/li>\n\n\n\n<li><strong>With no combinator, :has() matches at any depth \u2014 not just direct children.<\/strong> <code>.card:has(.icon)<\/code> matches whether <code>.icon<\/code> is an immediate child or buried five levels deep; the implicit combinator is the same &#8220;any descendant&#8221; one a plain space means everywhere else in CSS. If you specifically need &#8220;a direct child,&#8221; write <code>:has(&gt; .icon)<\/code> the way demo #4&#8217;s quantity query does \u2014 leaving the <code>&gt;<\/code> out there would let a stray <code>.qTile<\/code> nested inside some other element count toward the total by accident.<\/li>\n\n\n\n<li><strong>Well supported, but always worth a quick check for your audience.<\/strong> Every evergreen browser (Chrome, Edge, Safari, Firefox) has shipped <code>:has()<\/code> since late 2023. If your project needs to support meaningfully older browsers, check <a href=\"https:\/\/caniuse.com\/css-has\">caniuse.com\/css-has<\/a> for exact minimum versions before relying on it for anything load-bearing.<\/li>\n\n\n\n<li><strong>:has() can be expensive on deeply nested trees.<\/strong> Unlike most CSS selectors, which the browser can often reason about without walking much of the DOM, <code>:has()<\/code> may need to inspect a whole subtree to decide whether an element matches. For most real UI \u2014 cards, form fields, table rows \u2014 this is a non-issue. It&#8217;s worth being deliberate about it only if you&#8217;re applying a <code>:has()<\/code> rule broadly across a very large, deeply nested page.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is the CSS :has() selector?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>:has()<\/code> is a CSS pseudo-class that matches an element based on what&#8217;s found relative to it \u2014 a descendant by default, or a sibling with a combinator \u2014 rather than based on the element&#8217;s own attributes or state. It&#8217;s often called a &#8220;parent selector&#8221; because the most common use is styling an ancestor based on one of its children, but it also works for sibling relationships, as in the step-tracker hover-preview demo above.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the syntax of :has()?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>selector:has(relative-selector)<\/code>. The relative selector can start with a combinator \u2014 <code>&gt;<\/code> for a direct child only, <code>~<\/code> or <code>+<\/code> for a sibling \u2014 or omit one entirely, which defaults to matching any descendant at any depth. It also accepts a comma-separated list, matching if any one of the listed conditions is found.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I nest :has() inside :has()?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No \u2014 the CSS spec explicitly forbids it, and that restriction applies even if the inner <code>:has()<\/code> is wrapped inside a <code>:not()<\/code>. A selector like <code>.wrap:has(.list:not(:has(li)))<\/code> is invalid and the whole rule is ignored. Reach for a sibling combinator (<code>+<\/code> or <code>~<\/code>) instead when you find yourself wanting to nest, as demo #9&#8217;s empty-state pattern does above.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does :has() work with the :not() pseudo-class?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes \u2014 <code>:has(x:not(y))<\/code> and <code>:not(:has(x))<\/code> are both valid and genuinely useful; several demos above rely on exactly this combination, including the gated-submit-button (<code>:has(#terms:not(:checked))<\/code>) and the empty-state message (<code>:not(:has(li))<\/code>). The one thing <code>:not()<\/code> can&#8217;t do for you inside a <code>:has()<\/code> is contain a second <code>:has()<\/code> \u2014 see the pitfall above.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is :has() well supported in browsers?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, in every evergreen browser as of late 2023 \u2014 Chrome, Edge, Safari, and Firefox all ship it. If your project has to support older or less common browsers, verify the exact cutoff on <a href=\"https:\/\/caniuse.com\/css-has\">caniuse.com\/css-has<\/a> before depending on it for essential functionality.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can :has() replace JavaScript for interactive styling?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For a large class of &#8220;style something based on a checkbox, an input&#8217;s validity, or the presence of another element&#8221; interactions, yes \u2014 every demo above does exactly that with zero JavaScript driving the visual result. It doesn&#8217;t replace JavaScript outright, though: anything that needs to read state back out (a &#8220;3 selected&#8221; counter), persist a choice, or run real application logic still needs script, same as it always has.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every demo above ships its full HTML, CSS and JS in the <strong>HTML \/ CSS \/ JS<\/strong> tabs in its own top bar. But the better way to actually learn this is to click <strong>Fork &amp; Edit<\/strong> instead of just copying: it hands you the same snippet in a live editor where you can rename a class, delete a combinator, or swap a selector and watch exactly what stops working. Once you&#8217;ve got a version you like, save it to <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> and it&#8217;s yours to keep, tweak further, or reuse in a real project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>:has() is the CSS feature people spent a decade calling impossible: a parent selector. Before it, CSS could style a descendant based on its ancestor, but never the other way round \u2014 there was no way to say &#8220;style this &lt;div&gt; differently because a specific child is present inside it.&#8221; :has() closes that gap, and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":47,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":"","advanced_seo_description":"Learn the CSS :has() parent selector with 10 clickable live demos: cards, forms, tables, menus and more. Edit each one free in your browser, no sign-up needed.","jetpack_seo_html_title":"CSS :has() Selector: 10 Live Examples You Can Edit","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"Learn the CSS :has() parent selector with 10 clickable live demos: cards, forms, tables, menus and more. Edit each one free in your browser, no sign-up needed.","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[6,9],"tags":[],"class_list":["post-45","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-css","category-tutorials","et-has-post-format-content","et_post_format-et-post-format-standard"],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/phrh5V-J","jetpack_featured_media_url":"https:\/\/i0.wp.com\/webdevpuneet.com\/blog\/wp-content\/uploads\/2026\/10\/css-has-selector.webp?fit=1200%2C630&ssl=1","_links":{"self":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/45","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/comments?post=45"}],"version-history":[{"count":2,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/45\/revisions"}],"predecessor-version":[{"id":48,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/45\/revisions\/48"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media\/47"}],"wp:attachment":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media?parent=45"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/categories?post=45"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/tags?post=45"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}