Selectors tell CSS which elements a rule applies to. The core ones are type (p), class (.card), ID (#main), attribute ([type=email]), combinators (descendant space, child >, sibling + and ~), pseudo-classes such as :hover and :nth-child(2n), and pseudo-elements such as ::before. Type a selector below to see what it matches.
| Selector | Matches | Specificity |
|---|---|---|
| p | Every p element | 0,0,1 |
| .card | Elements with class card | 0,1,0 |
| #main | The element with id main | 1,0,0 |
| nav a | a anywhere inside nav | 0,0,2 |
| ul > li | li that are direct children of ul | 0,0,2 |
| a:hover | A link under the pointer | 0,1,1 |
Updated
Reading a selector table only goes so far. The tester below runs your selector through the browser's own engine against a small sample page, highlights every element it matches, explains invalid input and works out the specificity. The calculator underneath answers the eternal question of which items 3n+1 actually picks.
| Selector | Example | Matches |
|---|---|---|
| * | * | Every element |
| Type | button | All button elements |
| Class | .price | class contains price |
| ID | #checkout | id is checkout |
| Attribute present | [disabled] | Has the attribute at all |
| Exact value | [type="email"] | Value is exactly email |
| Starts with | [href^="https"] | Value starts with https |
| Ends with | [href$=".pdf"] | Value ends with .pdf |
| Contains | [class*="btn"] | Value contains btn anywhere |
| Word in list | [rel~="noopener"] | Space-separated list includes the word |
| Language prefix | [lang|="en"] | en or en- followed by anything |
| Case-insensitive | [href$=".PDF" i] | Matches .pdf, .PDF and so on |
| Combinator | Example | Matches |
|---|---|---|
| Descendant (space) | nav a | a at any depth inside nav |
| Child > | ul > li | li directly inside ul |
| Next sibling + | h2 + p | The p immediately after an h2 |
| Later siblings ~ | h2 ~ p | Every p after an h2 with the same parent |
| Selector list , | h1, h2 | Either one |
| Pseudo-class | Matches |
|---|---|
| :hover | Under a mouse pointer |
| :focus | Has keyboard or programmatic focus |
| :focus-visible | Focused and the browser judges a ring should show |
| :focus-within | Itself or a descendant has focus |
| :active | Being pressed |
| :visited | A link already visited (limited styling for privacy) |
| :checked / :disabled / :required | Form controls in that state |
| :valid / :invalid / :user-invalid | Form validation states; user-invalid waits for interaction |
| :placeholder-shown | An input currently showing its placeholder |
| :first-child / :last-child / :only-child | Position among siblings |
| :nth-child(An+B) / :nth-last-child() | Position by formula, counting all siblings |
| :nth-of-type() / :first-of-type | Position counting only the same tag |
| :empty | No children and no text |
| :not(x) | Does not match x |
| :is(x, y) / :where(x, y) | Matches any in the list; where has zero specificity |
| :has(x) | Contains, or is followed by, something matching x |
| :root | The html element |
| :target | The element whose id is in the URL hash |
| Pseudo-element | Targets |
|---|---|
| ::before / ::after | Generated content before or after the element's content |
| ::first-letter / ::first-line | First letter or first rendered line of a block |
| ::placeholder | Placeholder text in an input |
| ::selection | Text the user has highlighted |
| ::marker | The bullet or number of a list item |
| ::file-selector-button | The button of a file input |
| ::backdrop | The layer behind a modal dialog or fullscreen element |
Each decoration lives in CSS, so the HTML stays clean. content is required for both pseudo-elements.
<style>
label.req::after { content: " *"; color: #dc2626 }
a[href^="http"]::after { content: " \2197"; font-size: .8em }
blockquote { position: relative; margin: 12px 0 0; padding-left: 28px; font-style: italic }
blockquote::before { content: "\201C"; position: absolute; left: 0; top: -10px; font-size: 44px; color: #4F98FB }
</style>
<label class="req">Email</label>
<p><a href="https://example.com">Supplier price list</a></p>
<blockquote>The best bread in the neighbourhood.</blockquote>Tab into the field to see the ring. The card turns red only while it holds an invalid email.
<style>
.card { padding: 12px; border: 2px solid #e5e7eb; border-radius: 10px }
.card:has(input:invalid:not(:placeholder-shown)) { border-color: #dc2626 }
button:hover { background: #1e3a8a }
button, input { padding: 8px; margin-top: 6px }
button { background: #4F98FB; color: #fff; border: 0; border-radius: 8px }
:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px }
</style>
<div class="card"><label>Email <input type="email" placeholder="[email protected]"></label><br><button>Send</button></div>
Browsers match selectors from right to left. For nav ul li a the engine starts with every link on the page, then walks up to check for an li, a ul and a nav. This is why the rightmost part, called the key selector, matters most for speed, although on normal pages selector speed is rarely a real problem.
A comma makes a selector list, which is just several selectors sharing one rule. If any one of them is invalid, the whole rule is dropped, except inside :is() and :where(), which forgive invalid entries.
A pseudo-class, written with one colon, selects an existing element in a particular state or position: :hover, :checked, :first-child, :empty. A pseudo-element, written with two colons, creates or targets a part that is not its own element in the HTML: ::before, ::placeholder, ::selection, ::marker.
Only one pseudo-element is allowed per compound selector, and it must come last. Content added through ::before and ::after is usually read aloud by screen readers but cannot be selected or copied, so do not put essential information there.
Most specificity fights start when an ID or a long chain of nested selectors wins against a later, simpler rule. Flat class names, one or two levels deep at most, keep every rule easy to override.
Cascade layers (@layer) give another tool: rules in a later layer beat rules in an earlier one regardless of specificity. Resets and third-party styles can live in a low layer so your own classes always win.
:nth-child(2) counts every sibling element, then checks that the second one also matches the rest of the selector. :nth-of-type(2) counts only siblings of the same tag. So p:nth-child(2) matches nothing if the second child is an h2, while p:nth-of-type(2) still finds the second paragraph.
They are the same pseudo-element. The double colon was introduced to tell pseudo-elements apart from pseudo-classes, and browsers still accept the single colon for the four older ones (before, after, first-line, first-letter) for backward compatibility. Write the double colon in new code.
:has() matches an element based on what it contains or what follows it, for example .card:has(img) styles cards that hold an image, and label:has(+ input:invalid) styles a label before an invalid field. It is supported in all current major browsers, with Firefox the last to add it at the end of 2023.
Count three things: ID selectors; classes, attribute selectors and pseudo-classes; then type selectors and pseudo-elements. Compare from the left, so one ID beats any number of classes. :where() adds nothing, while :is(), :not() and :has() take the score of their most specific argument. Inline styles and !important sit above all of these.
Touch screens have no hover state, so browsers either skip it or apply it briefly after a tap. Wrap hover-only styling in @media (hover: hover) and make sure anything revealed on hover is also reachable by tap and keyboard.
Related: CSS specificity calculator, CSS box model, CSS cursor values, HTML and CSS cheat sheet, Modern CSS features.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.