CSS Selector & XPath Tester

Try out the patterns used to point at parts of a web page, both CSS selectors and XPath, and see exactly what they match. It also works out specificity, which is three numbers compared left to right: 256 classes still lose to one id, which is why adding another class stops working.

How to use it

Paste the markup you are actually dealing with rather than a tidied version of it, and every element your pattern matches is listed back, which is usually how the one extra match you did not want turns up.

  1. Paste some HTML A fragment is fine — it is parsed as a document.
  2. Write a selector Anything the browser understands, including :has() and :where().
  3. Read the specificity Three numbers: ids, then classes and attributes, then elements.
Full instructions

In depth

Why one CSS rule beats another, explained with real numbers

How specificity decides which style wins, what the three columns mean, why adding more classes eventually stops helping, how to test selectors and XPath against your own HTML, and how newer pseudo-classes change the calculation.

Screenshot: What matches, and how hard it wins
What matches, and how hard it wins as it appears when the page opens.

The cascade problem in one sentence

Two CSS rules point at the same element. Both set the same property. One wins. The question is always which one and why.

The answer is specificity, a scoring system that gives every selector a weight. When two selectors match the same element, the browser compares their weights and the heavier one applies. If the weights are equal, whichever comes later in the stylesheet wins.

The trouble is that the scoring is not a single number. It is three numbers compared from left to right, and that left-to-right comparison is the reason a trick that worked five minutes ago suddenly does nothing. Understanding this system means you can predict the winner before you open the browser, and you stop reaching for hacks.

The three columns

Every selector breaks down into three counts. The first column counts IDs. The second counts classes, attribute selectors, and pseudo-classes. The third counts element names and pseudo-elements.

The selector #nav .link a has one ID, one class, and one element, so its weight is 1-1-1. The selector .menu .item .active has zero IDs, three classes, and zero elements, so its weight is 0-3-0.

Comparison starts at the left. 1-0-0 beats 0-3-0 because one is greater than zero in the first column. The second and third columns are never even looked at. That is the key point: columns never carry. No number of classes can outweigh a single ID.

This is why piling on extra classes eventually stops working. Once a competing rule has an ID in its selector, any number of classes on your side is not enough to win. Some guides write specificity as a single number like 112 for 1-1-2, but that breaks as soon as any column passes nine. The three-column format is the correct one.

Using the tester

Paste HTML into the HTML box on the right side of the page. It can be a full page or just a fragment. The tool parses it into a document either way.

Type a selector into the Selector box above it. The results panel on the left shows every element that matches, listed with its tag name and enough context to identify it. The specificity is shown as the three-column figure.

The Language dropdown lets you switch between CSS selector and XPath. Both are run against the same parsed HTML, and the match list works the same way regardless of which language you choose.

The Sample button loads a small piece of HTML with some classes and IDs already in place, so you can experiment without preparing your own markup first. The Download button saves the full result as a text file.

Quick selector buttons

Below the selector input sit four small buttons that fill in common selector patterns so you can see specificity differences without typing anything.

id + class fills in a selector that mixes an ID with a class, showing how even one ID puts the first column above zero. :where() fills in a selector wrapped in :where(), which always contributes zero to specificity no matter what is inside it. :has() fills in a selector that targets a parent based on its children. :not() fills in a selector that excludes elements by a condition.

Each one updates the result immediately, so you can press them in sequence and watch the three columns change.

How :where and :is change the arithmetic

Older CSS had no way to write a selector with zero specificity. Every class you used raised the weight, even if you only meant it as a default that anything else should be able to override.

:where() fixes that. Whatever you put inside it contributes nothing to any column. A selector like :where(.sidebar) .link has the same weight as .link alone, because the :where wrapper erases the sidebar class from the count. This is the right tool for writing base styles that other rules can override easily.

:is() does the opposite. It takes the specificity of its most specific argument. :is(.big, #main) scores as if #main were written directly, giving the whole selector an ID column of one. :not() and :has() follow the same rule as :is and take the weight of whatever is inside them.

These distinctions are the reason the same selector can behave differently in a modern browser than in an older one that does not support these pseudo-classes.

XPath and what it can do that CSS cannot

CSS selectors can match elements by tag name, class, attribute, and structural position. What they cannot do is match by text content. There is no CSS selector that says find every paragraph containing the word hello.

XPath can. Switch the Language dropdown to XPath and type //p[contains(text(),"hello")]. It finds every p element whose text includes that word. XPath can also walk upward to a parent, which CSS only recently gained with :has(), and navigate between siblings in either direction.

XPath is more verbose than CSS, but for scraping, testing, and automation it covers cases that CSS still does not reach. The tool runs both against the same parsed HTML, so you can compare results side by side by switching back and forth.

One useful pattern is to find the XPath for an element first, then write the closest CSS selector you can manage and compare the match counts. If they differ, the gap tells you what CSS cannot express for that particular query.

Common mistakes and what beats what

  • Assuming more classes always win. 0-99-0 loses to 1-0-0. The columns are separate counters, and the first non-equal column decides the winner.
  • Forgetting that inline styles sit above all selectors. Only !important can override an inline style, and another !important later in the source order overrides that.
  • Using an ID when a class would do. An ID locks you into the highest normal specificity tier. Classes are easier to override later and work better in component-based designs.
  • Debugging by adding !important everywhere. Each one creates a new specificity contest among the !important rules, and you end up in the same problem one layer up.
  • Expecting the universal selector * to contribute weight. It adds nothing at all. Neither do combinators like >, +, and ~. They describe structure, not specificity.
  • Writing a selector that matches nothing and blaming the browser. Most often the problem is a misspelled class name, or expecting a descendant where the element is actually a sibling. The match list settles this instantly.

A worked example with conflicting rules

Suppose your HTML has a div with the ID hero that contains a paragraph with the class intro. Two rules target it. The first is #hero .intro and the second is .page .hero-section .intro .first-paragraph.

The first selector has one ID and one class: 1-1-0. The second has zero IDs and four classes: 0-4-0. Despite having far more parts, the second rule loses because the first column decides the contest before the second column is reached.

To fix this without restructuring the HTML, wrap the second selector in :is() with nothing extra, which does not help, or remove the ID from the first rule and replace it with a class. That moves both selectors into the same tier and lets the number of classes decide again.

Paste the HTML into the tool, try both selectors, and compare the specificity figures. The numbers make the answer obvious without any guessing.

Now try wrapping the second selector in :where(). The specificity drops to 0-0-0 because :where() erases every piece inside it. That makes the second rule the weakest possible, and any other rule overrides it. This is how library authors write default styles that any project-level class can beat.

Your HTML stays in your browser

The selector matching and the specificity calculation both happen on your own machine. The HTML you paste is never sent anywhere. There is no account, no size limit beyond what your browser can hold in memory, and no record of what you tested.

For testing against production markup that includes internal class names or layout details, that privacy matters. Load the page, go offline, and it still works. The browser is the only engine involved. Close the tab and every piece of pasted HTML is gone without a trace.

Help

What matches, and how hard it wins

Paste the markup you are actually dealing with rather than a tidied version of it, and every element your pattern matches is listed back, which is usually how the one extra match you did not want turns up. CSS and XPath run against the same pasted HTML, so a pattern can be rewritten from one into the other and checked. The specificity figure comes with any CSS selector you try.

Paste some HTML

A fragment is fine — it is parsed as a document.

Write a selector

Anything the browser understands, including :has() and :where().

Read the specificity

Three numbers: ids, then classes and attributes, then elements.

Your browser does the matching

Selectors run through the real engine, so support is exactly whatever your browser supports — including newer things like :has().

Good to know

Columns never carry

They are compared left to right. 0-256-0 loses to 1-0-0, which is why piling on classes eventually stops working and people reach for !important.

:where() adds nothing

Zero specificity whatever is inside it — the right tool for defaults you want anything to override. :not(), :is() and :has() take their most specific argument instead.

XPath can match on text

Which CSS still cannot. Switch language and use //p[contains(text(),"hello")].

Common questions

How is specificity written?
As three numbers, often "0-1-2". Some write it as a single number, which breaks as soon as any column exceeds nine.
Does the universal selector count?
No — * contributes nothing to specificity, as do combinators like > and +.
What beats an inline style?
Only !important, or another !important later in the cascade. Inline styles sit above every selector.
Why does my selector match nothing?
Most often a typo in a class name, or expecting a descendant where the element is a sibling. The match list shows exactly what the browser sees.