CSS Selectors Reference
The full set of selectors a browser's querySelectorAll accepts, with the attribute operators written out and the specificity contribution of each construct. Pseudo-classes that depend on a rendered page are listed separately, because they are the usual reason a selector that looks correct matches nothing in parsed markup.
Simple selectors
| Selector | Name | Matches | Example |
|---|---|---|---|
| * | universal | Every element. | * |
| type | type | Elements with that tag name. | p |
| .class | class | Elements whose class list contains that name. | .card |
| #id | id | The element with that id. | #main |
| [attr] | attribute presence | Elements that have the attribute at all, whatever its value. | [disabled] |
| [attr=v] | exact | Attribute value equal to v. | [type="text"] |
| [attr~=v] | whitespace list | Value is a space-separated list containing v as a whole word. | [rel~="noopener"] |
| [attr|=v] | hyphen prefix | Value is v, or starts with v followed by a hyphen. | [lang|="en"] |
| [attr^=v] | prefix | Value starts with v. | [href^="https://"] |
| [attr$=v] | suffix | Value ends with v. | [src$=".svg"] |
| [attr*=v] | substring | Value contains v anywhere. | [class*="col-"] |
| [attr=v i] | case flag | Same as the operator before it, compared case-insensitively. The s flag forces case-sensitivity. | [type="TEXT" i] |
Combinators
| Combinator | Name | Meaning | Example |
|---|---|---|---|
| space | descendant | B anywhere inside A, at any depth. | article p |
| > | child | B as a direct child of A. | ul > li |
| + | next sibling | B immediately after A, same parent. | h2 + p |
| ~ | subsequent sibling | Every B after A, same parent. | h2 ~ p |
Structural pseudo-classes
| Pseudo-class | Matches | Note |
|---|---|---|
| :first-child | An element that is the first child of its parent. | Counts element siblings only. Text nodes are ignored. |
| :last-child | The last child of its parent. | |
| :only-child | An element with no element siblings. | |
| :nth-child() | Positional match on An+B, odd, even, or a literal integer. | Counts all element siblings, whatever their type. |
| :nth-last-child() | The same, counting from the end. | |
| :first-of-type | First sibling with that tag name. | Counts only siblings of the same type. |
| :last-of-type | Last sibling with that tag name. | |
| :nth-of-type() | Positional match among siblings of the same tag name. | |
| :only-of-type | The only sibling with that tag name. | |
| :empty | An element with no children at all, text nodes included. | A single space between the tags defeats it. Comments do not. |
| :root | The document root, which is html in an HTML document. | |
| :not() | Elements not matching the argument. | Not forgiving: an invalid argument invalidates the whole selector. |
| :is() | Elements matching any branch of the argument list. | Forgiving: an invalid branch is dropped silently and the rest still match. |
| :where() | The same as :is(), with zero specificity. | Forgiving in the same way. |
| :has() | Elements containing something matching the argument. | Forgiving. Cannot be nested inside another :has(). |
Pseudo-classes that can never match parsed markup
| Pseudo-class | Why |
|---|---|
| :hover | Needs a live pointer position. |
| :focus | Needs a focused element, which requires a rendered document. |
| :focus-visible | Needs focus plus the browser's heuristic about how it was given. |
| :active | Needs a pointer or key held down right now. |
| :visited | Needs browsing history, and is deliberately restricted even on a live page. |
| :target | Needs the document's URL fragment. |
| :checked, :disabled, :required, :read-only | These DO work. They are backed by attributes in the markup, so they match on a parsed document like any attribute selector. |
These read state that only exists on a rendered page with a user in front of it. Against a parsed document they always return nothing, which is not a bug in your selector.
Specificity contributions
| Construct | a (id) | b (class) | c (type) |
|---|---|---|---|
| #id | 1 | 0 | 0 |
| .class | 0 | 1 | 0 |
| [attr] | 0 | 1 | 0 |
| pseudo-class, e.g. :first-child | 0 | 1 | 0 |
| type, e.g. div | 0 | 0 | 1 |
| pseudo-element, e.g. ::before | 0 | 0 | 1 |
| * | 0 | 0 | 0 |
| combinators ( > + ~ space ) | 0 | 0 | 0 |
| :where() | 0 | 0 | 0 |
| :is(), :not(), :has() | most specific argument | most specific argument | most specific argument |
| :nth-child(An+B of S) | 0 plus S | 1 plus S | 0 plus S |
Specificity is a triple compared left to right, and it is computed per selector, not across a comma-separated list. Each selector in a list carries its own value.
Frequently asked questions
Why does table > tr never match?
Because the HTML parser inserts a tbody element into any table that does not have one, so the rows are never direct children of the table. Use table > tbody > tr, or just table tr. The same markup parsed as XML keeps the rows where you wrote them, which is why the two parse modes disagree.
What is the difference between :is() and :not() when a branch is invalid?
It changes whether you get an error or a silently wrong answer. :is(), :where() and :has() take forgiving selector lists: a branch that does not parse is dropped and the rest still match, with no error reported anywhere. :not() is not forgiving, so an invalid argument throws a SyntaxError for the whole selector. A typo inside :is() is therefore much harder to notice than the same typo inside :not().
How is specificity actually compared?
As three numbers compared left to right, not as a base-10 figure. (1,0,0) beats (0,9,9) no matter how many classes are on the other side. Specificity is only consulted when two rules have the same origin and importance, and it is computed for each selector in a comma list independently.