CSS Selector Cheat Sheet
Every everyday CSS selector on one page, from attributes and combinators to :has() and ::marker, each with a working example and its specificity. Paste your own selectors into the calculator to see which one wins and why.
67 selectors
Universal
**, *::before, *::after {
box-sizing: border-box;
}Every element, with no specificity at all. Pseudo-elements are not elements, which is why resets list ::before and ::after separately.
Type
pp {
max-width: 65ch;
}Elements by tag name. Case does not matter for HTML tags, so P and p are the same.
Class
.card.card {
padding: 1.5rem;
}Any element whose class attribute lists card as one of its space-separated words. Class names are case-sensitive: .Card will not match class="card".
ID
#pricing#pricing {
scroll-margin-top: 5rem;
}The element with id="pricing". One ID outweighs any number of classes, so ID rules are hard to override later. Many teams keep IDs for anchors and scripts.
Compound
button.primarybutton.primary {
background: #0369a1;
}No space between the parts: one element has to match all of them. If there is a type selector, it goes first, so write button.primary, not .primarybutton.
Selector list
h1, h2, h3h1, h2, h3 {
line-height: 1.2;
}A comma separates alternatives that share the declarations, and each keeps its own specificity. If one entry is invalid, the browser drops the whole rule; inside :is() it is ignored instead.
Has the attribute
[disabled]button[disabled] {
cursor: not-allowed;
}The attribute is present, whatever its value, even an empty one.
Exact value
[type="email"]input[type="email"] {
min-width: 18rem;
}The whole value must match. Quotes are optional for a plain word, but values with spaces, dots or a leading digit need them.
One word in a list
[data-tags~="sale"][data-tags~="sale"] .price {
color: #b91c1c;
}The value is a space-separated list that contains this exact word, the way a class selector reads the class attribute. data-tags="new sale" matches; "wholesale" does not.
Value or value-
[lang|="en"][lang|="en"] q {
quotes: "“" "”";
}Exactly en, or en followed by a hyphen (en-GB, en-US). It was built for language codes, but :lang(en) does the job better because it also sees inherited lang.
Starts with
[href^="mailto:"]a[href^="mailto:"]::before {
content: "✉ ";
}The value begins with the string. Good for link types: mailto:, tel:, or https:// for external links.
Ends with
[href$=".pdf"]a[href$=".pdf"]::after {
content: " (PDF)";
}The value ends with the string. A link like report.pdf?v=2 will not match, because the query string comes last.
Contains
[class*="col-"][class*="col-"] {
min-width: 0;
}The string appears anywhere in the value, so this also matches class="no-col-gap". When you control the markup, a dedicated class is more precise.
Ignore case
[href$=".pdf" i]a[href$=".pdf" i]::after {
content: " (PDF)";
}The i flag compares the value case-insensitively, so .PDF and .Pdf match too. Every current browser supports i. The opposite flag, s, has very little support.
Descendant (space)
.card p.card p {
margin-block: 0.5em;
}A p anywhere inside .card, at any depth, including inside a nested card.
Child (>)
.menu > li.menu > li {
display: inline-block;
}Direct children only. List items in a submenu inside .menu are skipped.
Next sibling (+)
h2 + ph2 + p {
margin-top: 0;
}The p directly after an h2, with the same parent. Text between them does not count, but any element in between breaks the match.
Later siblings (~)
h2 ~ ph2 ~ p {
color: #475569;
}Every p after the h2 under the same parent, with anything in between. Like all combinators except :has(), it only looks forward, never back.
Hover
:hover@media (hover: hover) {
.card:hover {
transform: translateY(-2px);
}
}A pointer is over the element or one of its descendants. On touch screens it fires on tap and can stick, so wrap hover-only effects in @media (hover: hover) and never hide anything essential behind them.
Pressed
:active.button:active {
transform: translateY(1px);
}While the element is being pressed. :link, :visited, :hover and :active all count as one pseudo-class, so for links put them in that order (LVHA) or a later one will override an earlier one.
Focus
:focusinput:focus {
border-color: #0284c7;
}The element has focus by any route: keyboard, mouse or script.
Visible focus
:focus-visible.button:focus-visible {
outline: 3px solid #0284c7;
outline-offset: 2px;
}Focus that the browser judges should be shown: keyboard focus, and text fields however they were focused. Use it for focus rings instead of removing outline on :focus.
Focus inside
:focus-within.field:focus-within {
box-shadow: 0 0 0 3px #bae6fd;
}The element has focus or contains the focused element. It highlights a whole field group while someone types in its input.
Unvisited link
:linka:link {
color: #0369a1;
}An a or area element with an href that has not been visited. A plain <a> with no href matches neither :link nor :visited.
Visited link
:visiteda:visited {
color: #6d28d9;
}Links in the browsing history. To protect privacy, browsers only apply colour properties here, and scripts reading the computed style see the unvisited colours.
Any link
:any-link:any-link {
text-underline-offset: 0.2em;
}Every a or area with an href, visited or not. It is shorter than :link, :visited, and counts as one pseudo-class.
URL fragment target
:targetsection:target {
background: #fef9c3;
}The element whose id matches the #fragment in the address bar, for example #pricing. It changes when someone follows an in-page link.
Checked
:checkedinput:checked + label {
font-weight: 600;
}Ticked checkboxes, the selected radio button and selected <option>s. The + label pattern only works when the label comes right after the input.
Disabled
:disabledbutton:disabled {
opacity: 0.6;
}Controls with the disabled attribute, including controls inside a disabled <fieldset>. aria-disabled="true" does not count, so target that attribute separately.
Required
:requiredlabel:has(+ :required)::after {
content: " *";
}Inputs, selects and textareas with the required attribute. :optional matches the rest.
Invalid
:invalidform:invalid button[type="submit"] {
opacity: 0.6;
}The value breaks a constraint: required, type, pattern, min, max or length. An empty required field is :invalid from the moment the page loads, so error styles here show before anyone types.
Invalid after input
:user-invalidinput:user-invalid {
border-color: #dc2626;
}Like :invalid, but only once the person has changed the field and moved on, or tried to submit. It is the one to use for red error borders.
Placeholder showing
:placeholder-showninput:not(:placeholder-shown) + label {
transform: translateY(-1.2em) scale(0.85);
}The field is empty and showing its placeholder, which is the trick behind floating labels. The input needs a placeholder attribute; a single space is enough.
Indeterminate
:indeterminateinput[type="checkbox"]:indeterminate {
accent-color: #94a3b8;
}A checkbox set to indeterminate by script (the "some selected" state), a radio group with nothing chosen, or a <progress> without a value.
Root element
:root:root {
--brand: #0369a1;
}The top element, which is <html> in a web page. It has class-level specificity (0, 1, 0), whereas html has (0, 0, 1). That is one reason custom properties live here.
First child
li:first-childli:first-child {
border-top: 0;
}An li that is the first element among its siblings. If another element comes first, no li matches. That case calls for :first-of-type.
Last child
li:last-childli:last-child {
margin-bottom: 0;
}The last element among its siblings. Text and comments do not count, only elements.
Only child
:only-child.tag:only-child {
margin: 0;
}The element has no element siblings.
Nth child
tr:nth-child(even)tr:nth-child(even) {
background: #f8fafc;
}An+B counts from 1: 2n+1 or odd gives 1, 3, 5; 3n gives 3, 6, 9; -n+3 gives the first three. It counts every element sibling, whatever its type.
Nth child of a selector
tr:nth-child(even of :not([hidden]))tr:nth-child(even of :not([hidden])) {
background: #f8fafc;
}Counts only the siblings that match the selector after of, so zebra stripes stay even when rows are filtered out. The selector after of adds to the specificity.
Nth from the end
li:nth-last-child(-n+3)li:nth-last-child(-n+3) {
font-weight: 600;
}The same formula counted from the last sibling. -n+3 picks the last three.
First of its type
p:first-of-typearticle p:first-of-type {
font-size: 1.125rem;
}The first p among its siblings, even if an h2 or img comes before it. Note that "type" means tag name; .intro:first-of-type does not mean the first .intro.
Nth of its type
img:nth-of-type(3n).gallery img:nth-of-type(3n) {
margin-right: 0;
}Counts only siblings with the same tag name. :last-of-type, :nth-last-of-type() and :only-of-type work the same way.
Empty
:empty.alert:empty {
display: none;
}No children at all, including text. A single space or line break counts as content, so <div> </div> is not :empty. Comments are ignored.
Not
.tab:not(.active).tab:not(.active) {
opacity: 0.7;
}Elements that do not match. It accepts a list, as in :not(.a, .b), and takes the specificity of its most specific argument, so :not(#x) adds an ID.
Is (any of)
:is(h1, h2, h3) a:is(h1, h2, h3) a {
text-decoration: none;
}Matches if any argument matches, which saves writing out every combination. An invalid argument is skipped instead of breaking the rule. Its specificity is that of the most specific argument.
Where (zero weight)
:where(ul, ol):where(ul, ol)[class] {
list-style: none;
padding: 0;
}Works like :is() but adds zero specificity, whatever is inside. Use it for defaults and resets that any later rule should be able to override.
Has (parent selector)
.card:has(> img).card:has(> img) {
padding-top: 0;
}An element containing a match: here, a card with an img as a direct child. h2:has(+ p) checks the next sibling instead. Supported in Chrome 105, Safari 15.4 and Firefox 121 and later.
Language
:lang(de):lang(de) {
hyphens: auto;
}Matches by language, including lang inherited from an ancestor and longer codes such as de-AT. [lang="de"] only checks the element’s own attribute.
Direction
:dir(rtl).icon-arrow:dir(rtl) {
transform: scaleX(-1);
}Matches by text direction, whether inherited or detected by dir="auto". [dir="rtl"] only sees the attribute itself.
Defined
:definedmy-widget:not(:defined) {
visibility: hidden;
}Built-in elements, plus custom elements once their definition has loaded. :not(:defined) hides a web component until its script runs.
Open popover
:popover-open[popover]:popover-open {
opacity: 1;
}An element with the popover attribute while it is showing.
Modal
:modaldialog:modal {
max-width: 32rem;
}A dialog opened with showModal() rather than show(), or an element in fullscreen. While a modal dialog is open, the rest of the page is inert.
Before
::before.badge::before {
content: "";
width: 0.5rem;
aspect-ratio: 1;
border-radius: 50%;
background: currentColor;
}A generated first child. It only appears with a content property, even an empty one, and not on img or most inputs, which have no inside.
After
::after.external::after {
content: " ↗" / "";
}A generated last child. The text after the slash is the alternative text for screen readers, and an empty string keeps decorative symbols out of the spoken name.
First line
::first-line.lead::first-line {
font-variant: small-caps;
}The first rendered line of a block, wherever it wraps at the current width. Only font, colour, background and text properties apply.
First letter
::first-letter.story > p:first-of-type::first-letter {
float: left;
font-size: 3.5em;
line-height: 1;
}The first letter of a block, together with any punctuation before it. This is the usual way to make a drop cap.
Placeholder text
::placeholderinput::placeholder {
color: #64748b;
}The placeholder text in an empty field. Pale placeholder text often fails contrast checks, and it disappears as soon as someone types, so it should never be the only label.
Selected text
::selection::selection {
background: #fde68a;
color: #0f172a;
}Text the person has highlighted. Only a few properties apply, mainly color, background-color, text-decoration and text-shadow.
List marker
::markerli::marker {
color: #0369a1;
content: "→ ";
}The bullet or number of a list item, or the triangle of a <summary>. It takes colour, font properties, content and a few others; layout properties are ignored.
File input button
::file-selector-buttoninput[type="file"]::file-selector-button {
border-radius: 0.5rem;
}The "Choose file" button inside <input type="file">.
Backdrop
::backdropdialog::backdrop {
background: rgb(15 23 42 / 0.6);
}The layer behind a modal dialog, a popover or a fullscreen element, covering the rest of the page.
Shadow part
::part(label)my-button::part(label) {
font-weight: 600;
}An element inside a web component’s shadow root that the component exposes with part="label". Other elements inside the shadow root cannot be selected from outside.
Slotted content
::slotted(img)/* inside the component’s shadow root */
::slotted(img) {
max-width: 100%;
}Used in a shadow root’s stylesheet. It styles elements from the page that are placed in a <slot>, but only the top-level ones, not their descendants.
Nesting selector
&from the parent.card {
&:hover { border-color: #0369a1; }
& + & { margin-top: 1rem; }
.dark & { background: #0f172a; }
}In nested CSS, & stands for the parent rule’s selector. It counts like :is(parent), so if the parent is #a, .b, every match gets ID specificity.
Shadow host
:host/* inside the shadow root */
:host {
display: block;
}
:host(.compact) {
padding: 0.25rem;
}Inside a shadow root, the custom element itself. :host(.compact) matches when the element has that class on the page.
Scope root
:scope@scope (.card) {
:scope { border: 1px solid #cbd5e1; }
img { border-radius: 0.5rem; }
}The root of an @scope block. In JavaScript, el.querySelectorAll(":scope > li") means the direct children of el. In a plain stylesheet it means the same as :root.
How a browser matches a selector
Browsers read a selector from right to left. For .card > h2 they take each h2 and then check whether its parent has the class card. The part on the far right decides which elements are even considered; everything to its left is a filter on those elements’ ancestors or siblings.
<article class="card">
<h2>Account</h2>
<div class="details">
<h2>Billing</h2>
</div>
</article>.card h2 matches both headings, because a space allows any depth. .card > h2 matches only Account: Billing’s parent is .details, not the card. Both selectors have specificity 0, 1, 1. A combinator changes which elements match but never adds weight, so choose it for the relationship you need and settle conflicts some other way.
Siblings work the same way. h2 + p is only the paragraph directly after the heading, and h2 ~ p is every later paragraph under the same parent. Neither can look backwards or up to a parent; that is what :has() is for.
Counting specificity
Specificity is three separate counts, written A, B, C. When two rules set the same property on the same element, compare A first, then B, then C. The first difference decides, and a higher count in a later column never makes up for a lower one in an earlier column. Eleven classes (0, 11, 0) still lose to one ID (1, 0, 0).
| Column | Counts | Examples |
|---|---|---|
| A | ID selectors | #pricing |
| B | Classes, attribute selectors, pseudo-classes | .card [type="email"] :hover |
| C | Type selectors and pseudo-elements | button ::before :after |
| Nothing | Universal selector, combinators, :where() and everything inside it | * > + ~ :where(#a) |
| Argument | :is(), :not(), :has(): their most specific argument | :not(#x) = 1, 0, 0 |
| Parent | Nesting &: the parent selector, counted like :is() | & in #a, .b {…} = 1, 0, 0 |
Two edge cases trip people up. :root is a pseudo-class, so it weighs 0, 1, 0 while html weighs 0, 0, 1, even though both select the same element. And the old one-colon forms :before, :after, :first-line and :first-letter still count as pseudo-elements in column C, not as pseudo-classes.
Worked example: the link that stays grey
A sidebar menu marks the current page with a class, but the active link refuses to turn blue:
<nav id="sidebar">
<ul class="nav">
<li><a class="link is-active" href="/billing">Billing</a></li>
</ul>
</nav>
#sidebar a { color: #64748b; } /* 1, 0, 1 */
.nav .link.is-active { color: #0369a1; } /* 0, 3, 0 */Compare column A first: 1 against 0. The grey rule wins there and then, so the three classes in the blue rule never come into it, and moving the blue rule lower in the file changes nothing. Paste both lines into the calculator above (or press Load Example in Specificity mode) to see the breakdown.
Three ways to fix it, from best to worst:
- Lower the base rule.
:where(#sidebar) akeeps the same matching but weighs0, 0, 1, so the state rule at0, 3, 0wins. - Put base styles in a cascade layer. Wrap the sidebar rule in
@layer base { … }. Unlayered rules beat layered ones before specificity is checked at all, so the ID no longer matters. - Raise the state rule to
#sidebar .link.is-active(1, 2, 0). It works, but every later override now has to start with an ID too.
Adding !important also “works”, but it moves the declaration into a different part of the cascade, and the only thing that beats it later is another !important.
:is(), :where(), :not(), :has() and “of”
| Pseudo-class | Specificity | An invalid argument… |
|---|---|---|
| :is(.a, #b) | Most specific argument: 1, 0, 0 | is skipped; the rest still match |
| :where(.a, #b) | Always 0, 0, 0 | is skipped |
| :not(.a, #b) | Most specific argument: 1, 0, 0 | drops the whole rule |
| :has(> img, .b) | Most specific argument: 0, 1, 0 | drops the whole rule |
| :nth-child(2n of .b) | Pseudo-class + argument: 0, 2, 0 | drops the whole rule |
The “most specific argument” rule applies even when the element matched through a lighter argument. :is(.card, #featured) > h2 weighs 1, 0, 1 for every h2 it matches, including those inside a plain .card. If you want grouping without that side effect, use :where() and the same selector weighs 0, 0, 1.
Skipping invalid arguments makes :is() and :where() handy for vendor-specific pseudo-classes. A plain list such as .btn:focus-visible, .btn:-moz-focusring is dropped entirely by browsers that do not know the Firefox-only part, while .btn:is(:focus-visible, :-moz-focusring) keeps working everywhere. Pseudo-elements cannot go inside :is(), so for those, write separate rules. :has() briefly behaved this way too, but browsers changed it in 2023, so an invalid argument now invalidates it.
li:nth-child(2n of .visible) counts only the visible siblings, so stripes stay even when rows are hidden. li.visible:nth-child(2n) looks similar but counts every sibling and then checks the class, so a hidden row throws the pattern out.
Where specificity sits in the cascade
Specificity only settles a conflict once everything above it in this list is equal. The browser works down the list and stops at the first step that separates the two declarations:
- Origin and importance. Your
!importantdeclarations beat your normal ones, and normal author styles beat the browser’s defaults.!importantis not a fourth column of specificity; it comes first. - Context. With shadow DOM, normal page styles beat a component’s internal styles, and the order flips for
!important. - Style attribute.
style="…"beats any selector. It has no specificity of its own; it simply wins at this step. - Cascade layers. Later layers beat earlier ones, and unlayered styles beat all layers. For
!importantthe layer order is reversed. - Specificity, as calculated on this page.
- Scope proximity. Inside
@scope, the rule whose scope root is fewer steps up from the element wins. - Order of appearance. The last declaration in the CSS wins.
The calculator here only does step 5. It reads selector text and does not see your HTML or the rest of your stylesheet. When a declaration loses, browser developer tools show it struck through, and the Computed panel shows the rule that won.
Selectors that look right but do not match
| Selector | Why it misses | Fix |
|---|---|---|
| .card .title | Space means “inside”, but the classes are on the same element | .card.title |
| li:first-child | Another element, such as a heading or template, comes first | li:first-of-type |
| .item:first-of-type | “Type” means tag name, not class | .item:nth-child(1 of .item) |
| .alert:empty | A space or line break inside counts as content | Print the element with nothing between the tags |
| [class^="btn"] | class="large btn-x" starts with “large” | Give it a real class, or use [class*="btn-"] |
| h2 + p | Another element sits between them, or they have different parents | h2 ~ p, or check the markup |
| a::before | No content property | Add content: "" |
| img::after | Images and most inputs have no inside to put it in | Wrap the image and style the wrapper |
| input:invalid | Empty required fields are red before anyone types | input:user-invalid |
| a:visited | Only colour properties apply, for privacy | Change colours only |
| .btn:focus-visible, .btn:-moz-focusring | Browsers that do not know one entry drop the whole rule, including the part they do know | .btn:is(:focus-visible, :-moz-focusring), or separate rules |
The same selectors in JavaScript
querySelector(), querySelectorAll(), matches() and closest() all take the same selector syntax as CSS, with a few differences worth knowing:
list.querySelectorAll(':scope > li'); // direct children of list only
button.closest('.card'); // nearest ancestor (or itself) with .card
field.matches(':user-invalid'); // current state, true or false
document.querySelector('p::before'); // always null: pseudo-elements are not in the DOM
document.querySelector('#' + CSS.escape('1st-step')); // "#\31 st-step"- An invalid selector throws a
SyntaxErrorinstead of being ignored, so wrap user-supplied selectors intry/catch. - IDs and classes that start with a digit, such as
id="1st-step", are valid HTML but not valid CSS names.CSS.escape()turns them into the escaped form, and the calculator above accepts escaped names too. querySelectorAll()returns a snapshot. Elements added later are not in it, unlike the live collection fromgetElementsByClassName().- To read a pseudo-element’s styles, use
getComputedStyle(element, '::before').
Newer selectors and browser support
Everything else on this page has worked in every browser for years. These are the selectors where the version still matters, with the first version that supports each one fully. Edge follows Chrome.
| Selector | Chrome | Firefox | Safari |
|---|---|---|---|
| :not() with a list | 88 | 84 | 9 |
| :is() and :where() | 88 | 78 | 14 |
| :focus-visible | 86 | 85 | 15.4 |
| :has() | 105 | 121 | 15.4 |
| :modal | 105 | 103 | 15.6 |
| :nth-child(… of …) | 111 | 113 | 9 |
| :popover-open | 114 | 125 | 17 |
| :user-invalid / :user-valid | 119 | 88 | 16.5 |
| :dir() | 120 | 49 | 16.4 |
| Nesting with & (current syntax) | 120 | 117 | 17.2 |
To use one of these with a fallback, test for it with @supports selector(:has(a)). Browsers that do not understand the selector skip the block.
Frequently asked questions
What is the difference between .a .b and .a.b?
.a .b (with a space) is an element with class b somewhere inside an element with class a. .a.b (no space) is one element that has both classes.
Is there a parent selector in CSS?
Yes, :has(). .card:has(img) selects a card that contains an image. It works in Chrome 105, Safari 15.4, Firefox 121 and later.
Why do eleven classes not beat one ID?
Specificity columns do not carry over like digits in a number. The ID column is compared first, and 1 beats 0 no matter what the class column holds.
Does !important have a specificity?
No. Importance is checked before specificity, so an !important declaration beats a normal one however specific the normal one is. Specificity only decides between two !important declarations.
What specificity does an inline style have?
None, strictly. A style attribute wins at its own step of the cascade, before specificity is compared, so it beats every selector unless the selector rule uses !important.
How do I lower the specificity of a selector without changing what it matches?
Wrap the heavy part in :where(), which counts as zero. #nav a becomes :where(#nav) a at 0, 0, 1. Cascade layers are the other option when a whole set of rules should give way.
Sources
- W3C: Selectors Level 4: selector syntax, specificity rules, forgiving lists, :nth-child(… of …)
- W3C: CSS Cascading and Inheritance Level 6: cascade sort order, layers, scope proximity
- W3C: CSS Nesting Module: the & selector and its specificity
- WHATWG HTML Standard: pseudo-classes: :disabled, :checked, :link, :modal and other HTML-defined states
- MDN: Specificity: worked specificity examples
- MDN: :visited: privacy limits on visited-link styles
- MDN: CSS.escape(): escaping IDs and classes for selectors
- MDN browser-compat-data: browser versions in the support table
Related Tools
Build and preview common CSS Grid layout patterns.
Build linear and radial gradients with custom color stops.
Remove, replace, and export image metadata.
Generate and inspect solid background-color declarations.
Generate gradient and image background-image CSS.
Generate uniform or side-specific CSS border declarations.