Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Dev.to
Discord
WhatsApp Channel
daily.dev
Hashnode
X

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /
  3. Tools
  4. /
  5. CSS :has() Playground
Free · Live · No account

CSS :has() Playground

Pick a real-world HTML scene, type a :has() selector, and see exactly which elements match, live in your browser. No CodePen, no setup.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

How the playground works

The playground is not a screenshot or a canned animation. Your selector is run against real DOM nodes in the preview pane, so what you see is what a browser would actually match.

  1. 1

    Pick a scene

    Six preloaded scenes cover the situations where :has() earns its keep: form validation, an active nav item, content-aware cards, todo checkboxes, sibling dimming on hover, and a conditional clear button. Each scene loads real HTML with its own scoped styles into the preview pane.

  2. 2

    Type a selector

    The editor starts with a working selector for the scene, and the hint underneath tells you what is in the DOM (how many cards, which input is invalid, which link is active). You can edit it freely or write something completely different.

  3. 3

    The selector runs against the preview

    About 150 milliseconds after you stop typing, the tool calls document.querySelectorAll on the preview container with your selector. The debounce is what keeps typing smooth: without it, every keystroke would re-query the DOM on a half-written selector.

  4. 4

    Matched elements are outlined

    Every returned element gets a .pg-match class, which applies a pulsing outline. The match count above the preview tells you exactly how many nodes came back, which is the fastest way to tell an over-broad selector from a precise one.

  5. 5

    Show Answer applies the real CSS

    Highlighting proves which elements match. Show Answer goes further and injects the scene's actual CSS rule into the preview, so you see the styling effect rather than just the outline. This is the only way to demonstrate hover-based selectors, which cannot be captured by a one-shot query.

What the results mean

There are four outcomes after a query runs. Three of them tell you something useful about your selector, and one of them is a limitation of the tool rather than a mistake in your CSS.

Matches found: The normal case

One or more elements are outlined and the match count is above zero. Read the count carefully: matching six cards when you meant to match two usually means your :has() argument is broader than you think. The argument is matched against descendants, not just direct children, unless you add a combinator.

.card:has(img)          -> 2 matches  (two cards contain an img)
.card                   -> 3 matches  (every card, :has() removed)
.card:not(:has(img))    -> 1 match    (the text-only card)
Zero matches: Valid selector, nothing matched

The selector parsed fine but nothing in the current scene satisfies it. Usually the scene simply does not contain what you asked for, so switch scenes or relax the condition. Drop the :has() part first: if the bare selector matches and the :has() version does not, the argument is the problem.

.form-group:has(select)   -> 0 matches (this scene has no <select>)
.form-group               -> 3 matches (the containers do exist)
.form-group:has(input)    -> 3 matches (the argument was the issue)
Zero matches on a hover or focus selector: Expected, not a bug

querySelectorAll evaluates the selector at the instant it is called. Your pointer is over the editor at that moment, not over a card, so :hover is false for every element and the query returns nothing. The CSS itself is perfectly valid. Use Show Answer to apply the rule, then hover the preview to see it work.

.grid:has(.card:hover) .card:not(:hover)
# 0 matches from querySelectorAll, works fine as real CSS.

# The same applies to :focus, :focus-within, and :active.
Invalid selector syntax: The browser rejected it

The selector could not be parsed at all, so no query ran. Nearly always an unbalanced bracket, a stray comma, or a pseudo-element inside :has(). Pseudo-elements are not real elements, so they can never be the argument of :has().

.card:has(img            # unclosed parenthesis
.card:has()              # empty argument
li:has(a.active,)        # trailing comma
.card:has(::before)      # pseudo-elements are not allowed

:has() syntax reference

:has() takes a list of relative selectors and matches the element it is attached to, not the element inside the parentheses. That inversion is the whole point: it is the first selector in CSS that lets a parent respond to its children.

The basic form
/* Reads: any .card that contains an img, anywhere inside it. */
.card:has(img) { border-color: blue; }

/* The element that gets styled is .card, NOT the img.
   Everything before :has() is the subject of the selector. */

/* Descendant is the default: this matches a deeply nested img too. */
.card:has(figure > picture > img) { }
Combinators inside the parentheses
/* Direct child only: an img that is an immediate child of .card. */
.card:has(> img) { }

/* Next sibling: an h2 immediately followed by a p. */
h2:has(+ p) { }

/* Any later sibling: an h2 with a .warning somewhere after it. */
h2:has(~ .warning) { }

/* Without a leading combinator, the browser assumes a descendant. */
.card:has(img)   is the same as   .card:has(:scope img)
Multiple arguments, AND, and NOT
/* Comma inside :has() means OR: contains an img OR a video. */
.card:has(img, video) { }

/* Chaining means AND: contains an img AND contains an h3. */
.card:has(img):has(h3) { }

/* Negation, outside: cards that do NOT contain an img. */
.card:not(:has(img)) { }

/* Negation, inside: cards that contain something other than an img. */
.card:has(:not(img)) { }

/* Those last two are different. The first is about absence,
   the second is about the presence of a non-img descendant. */
Combining with state pseudo-classes
/* Form row turns red when its input fails validation. */
.form-group:has(input:invalid) { border-left: 3px solid #e53e3e; }

/* Label strikes through when its checkbox is checked. */
label:has(input:checked) { text-decoration: line-through; }

/* Clear button appears only when the input has a value.
   :placeholder-shown is true while the field is empty. */
.input-wrapper:has(input:not(:placeholder-shown)) .clear-btn { display: flex; }

/* Sibling dimming: every non-hovered card fades. */
.grid:has(.card:hover) .card:not(:hover) { opacity: 0.4; }

/* Lock page scroll while a dialog is open, with no JavaScript. */
body:has(dialog[open]) { overflow: hidden; }
Rules and limits
/* :has() cannot be nested inside another :has(). */
.a:has(.b:has(.c)) { }        /* invalid */

/* Pseudo-elements cannot be the argument. */
.card:has(::after) { }        /* invalid */

/* :has() cannot follow a pseudo-element. */
.card::before:has(img) { }    /* invalid */

/* Specificity: :has() itself adds nothing, but its most
   specific argument counts. */
.card:has(img)   -> (0, 1, 1)   class + element
.card:has(#hero) -> (1, 1, 0)   the id inside still counts

/* An invalid argument does NOT kill the whole rule, because
   :has() takes a forgiving selector list. */
.card:has(img, ::nonsense) { }   /* still matches on img */

One practical consequence of that last rule: a typo inside :has() fails silently instead of throwing. If a rule is not applying and the syntax looks right, check the argument in the playground above to confirm it actually matches something.

Try these patterns

Paste any selector from this table directly into the tool above.

SelectorWhat it selects
.form-group:has(input:invalid)Form row when its input is invalid
li:has(a.active)Nav item containing the active link
.card:has(img)Card containing an image
.grid:has(.card:hover) .card:not(:hover)Sibling cards (dimming on hover)
label:has(input:checked)Label whose checkbox is checked
.grid:has(:nth-child(3))Grid with 3+ items

What works vs. what doesn't in this playground

  • ✓Persistent states: :has(img), :has(.active), :has(input:invalid), :has(input:checked)
  • ✗Transient states: :has(:hover), :has(:focus) — evaluated at call time, not during hover. Click “Show Answer” to see the CSS effect directly.

Frequently Asked Questions

How does the element highlighting work?
  1. When you type a selector, the tool runs document.querySelectorAll(selector) inside the preview pane.
  2. It adds a .pg-match CSS class to every matched element, which applies a pulsing yellow outline.
  3. The query runs 150ms after each keystroke so the preview stays responsive while you type.
Why doesn't the hover-based selector highlight any elements?

querySelectorAll evaluates the selector at the exact moment it's called. Since nothing is hovered when the query runs, .grid:has(.card:hover) matches zero elements.

💡 Tip

The CSS itself still works: click Show Answer to apply the styles directly, then hover over the cards to see the effect live.

Why does :has(input:checked) work but :has(input:hover) doesn't?
:has(input:checked) ✓:has(input:hover) ✗
State typePersistent (DOM tracks it)Transient (only exists during hover)
querySelectorAllEvaluates real DOM stateHover is gone by call time
Works in playground?YesNo, use Show Answer instead
Is CSS :has() safe to use in production?

Yes. :has() is Baseline Widely Available since 2024. Chrome, Firefox, Safari, and Edge all support it fully. Global coverage exceeds 95% in 2026. Use it without fallbacks for any modern project.

How is this different from CodePen or JSFiddle?
This playgroundCodePen / JSFiddle
Starting point6 preloaded real-world scenesBlank canvas (build HTML yourself)
PurposeTest :has() specificallyGeneral CSS/JS sandbox
Account needed?NoOptional but needed to save

Related reading

Guide

8 CSS :has() Patterns You'll Actually Use

Form states, sibling dimming, modal scroll-lock, and five more real-world :has() patterns with copy-paste CSS and HTML.

Tool

CSS Specificity Calculator

Paste any CSS selector and see its (a, b, c) score. Understand how :has() affects specificity when you combine it with IDs and classes.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.