Skip to main content

CSS Syntax, Selectors, and Combinators

To style any element, you need to know two things: how to target it (the selector) and how to correctly write the rule (the syntax). This page covers every selector type and combination you'll use in real production CSS.


1. CSS Rule Anatomy

Every CSS rule follows the same structure:

/* selector { property: value; } */

h1 {
font-size: 2rem; /* declaration */
color: #1a1a1a; /* declaration */
line-height: 1.2; /* declaration */
} /* declaration block */
PartDescriptionExample
SelectorTargets the HTML element(s)h1, .c-btn, #nav
PropertyThe CSS attribute to changefont-size, color
ValueWhat to set the attribute to2rem, #1a1a1a
DeclarationOne property + value paircolor: red;
Declaration BlockAll declarations inside { }{ color: red; margin: 0; }
Rule / RulesetSelector + declaration blockThe complete CSS rule

2. CSS Comments

/* This is a single-line CSS comment */

/*
This is a
multi-line CSS comment
*/

.selector {
color: red; /* Inline comment — explains this specific value */
}

[!IMPORTANT] CSS uses only /* */ for comments. The // double-slash comment (common in JavaScript) is invalid in plain CSS and causes parse errors in some browsers.


3. Basic Selectors

Element Selector

Targets all elements of a specific HTML tag:

h1 { font-size: 2.5rem; }
p { line-height: 1.7; }
a { color: var(--color-primary); }
img { max-width: 100%; display: block; }

Class Selector (.)

Targets elements with a specific class attribute — the most common selector in production CSS:

.c-btn { padding: 0.7rem 1.5rem; border-radius: 9999px; }
.c-card { background: var(--color-surface); border-radius: 1rem; }
.u-hidden { display: none !important; }

ID Selector (#)

Targets a single unique element. Very high specificity — use sparingly:

#main-nav { position: sticky; top: 0; } /* Usually better as .c-nav */
#contact-form { max-width: 600px; }

[!TIP] In methodology-based CSS (BEM, ITCSS), ID selectors are almost never used for styling — they create specificity problems. Reserve #id for JavaScript hooks or anchor links.

Universal Selector (*)

Targets every element on the page:

/* The most important universal rule — always include in your reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

Attribute Selector ([attr])

Targets elements based on their HTML attributes:

/* Has attribute */
[disabled] { opacity: 0.5; cursor: not-allowed; }

/* Exact value */
[type="submit"] { background: var(--color-primary); }
[type="email"] { -webkit-appearance: none; }

/* Contains value (space-separated word) */
[class~="btn"] { cursor: pointer; }

/* Starts with */
[href^="https"] { /* external link */ }
[href^="mailto"] { /* email link */ }

/* Ends with */
[href$=".pdf"] { /* PDF link */ }
[src$=".svg"] { /* SVG image */ }

/* Contains substring */
[class*="card"] { border-radius: var(--radius-lg); }

/* Case-insensitive match */
[href$=".PDF" i] { /* matches .pdf, .PDF, .Pdf */ }

Grouping Selector (,)

Apply the same styles to multiple selectors in one rule:

h1, h2, h3, h4 {
font-family: var(--font-heading);
line-height: 1.2;
font-weight: 700;
}

input, select, textarea { font: inherit; }

.c-btn--primary:hover,
.c-btn--primary:focus-visible {
background: var(--color-primary-dark);
}

4. Combinator Selectors

Combinators describe the relationship between selectors:

Descendant Combinator (space)

Targets elements anywhere inside another element — at any depth:

/* All <a> tags anywhere inside .c-nav */
.c-nav a { text-decoration: none; color: var(--color-text); }

/* All <p> inside .c-article */
.c-article p { line-height: 1.8; margin-bottom: 1em; }

Child Combinator (>)

Targets elements that are direct children only (not deeper descendants):

/* Only direct <li> children of .c-nav__list */
.c-nav__list > li { display: flex; align-items: center; }

/* Only direct children — prevents styling nested lists */
.c-accordion > .c-accordion__item { border-bottom: 1px solid var(--color-border); }

Adjacent Sibling Combinator (+)

Targets the element immediately after another element with the same parent:

/* Paragraph immediately after a heading */
h2 + p { font-size: 1.125rem; color: var(--color-text-muted); }

/* Remove top margin on label immediately after checkbox */
.c-checkbox + label { margin-top: 0; }

/* Add top margin to every h2 except the first one */
h2 + h2 { margin-top: 3rem; }

General Sibling Combinator (~)

Targets all siblings after the specified element:

/* All <p> elements after an <h2> at the same level */
h2 ~ p { color: var(--color-text-muted); }

/* CSS-only accordion: all .c-accordion__content after checked input */
.c-accordion__toggle:checked ~ .c-accordion__content { display: block; }

/* Zebra table rows using sibling */
tr ~ tr { border-top: 1px solid var(--color-border); }

5. Pseudo-Class Selectors

Pseudo-classes target elements based on their state or position:

User Interaction States

a:hover { color: var(--color-primary); }
button:hover { transform: translateY(-2px); }
input:focus { border-color: var(--color-primary); }
input:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
button:active { transform: translateY(0); }
input:disabled { opacity: 0.5; cursor: not-allowed; }

Form States

input:placeholder-shown { border-style: dashed; } /* Placeholder showing */
input:not(:placeholder-shown) { border-style: solid; } /* Has real value */
input:valid { border-color: var(--color-success); }
input:invalid { border-color: var(--color-danger); }
input:user-valid { border-color: var(--color-success); } /* Only after user interaction */
input:user-invalid { border-color: var(--color-danger); } /* Modern — use this */
input:required { border-left: 3px solid var(--color-primary); }
input:optional { border-left: none; }
input:checked { /* For checkboxes/radios */ }

Structural / Position

li:first-child { border-top: none; } /* First child element */
li:last-child { margin-bottom: 0; } /* Last child element */
li:nth-child(2) { background: red; } /* Specific position */
li:nth-child(odd) { background: var(--color-surface); } /* 1, 3, 5... */
li:nth-child(even) { background: var(--color-surface-2); } /* 2, 4, 6... */
li:nth-child(3n) { /* Every 3rd: 3, 6, 9... */ }
li:nth-child(3n+1) { /* 1, 4, 7, 10... */ }

:first-of-type { /* First of its tag type in parent */ }
:last-of-type { /* Last of its tag type in parent */ }
:only-child { /* Only child of its parent */ }
:empty { display: none; } /* Has no children or text */
:root { /* <html> element — used for CSS custom properties */ }

Modern Functional Pseudo-Classes

/* :is() — matches any selector in the list (forgives invalid selectors) */
:is(h1, h2, h3, h4, h5) {
font-family: var(--font-heading);
line-height: 1.2;
}

/* :not() — excludes elements */
.c-nav__link:not(.is-active) { color: var(--color-text-muted); }
li:not(:last-child) { border-bottom: 1px solid var(--color-border); }
:not(p) { /* Everything that isn't a <p> */ }

/* :where() — same as :is() but specificity is ALWAYS zero */
:where(h1, h2, h3) { margin-bottom: 0.5em; } /* Easy to override */

/* :has() — the parent selector — targets a parent based on its children */
/* Chrome 105+, Firefox 121+, Safari 15.4+ */
.c-form__group:has(input:invalid) { border-color: var(--color-danger); }
.c-card:has(img) { padding: 0; } /* Card with image: no top padding */
.c-nav:has(.is-open) { background: rgba(0,0,0,0.9); }
section:has(> h2:first-child) { padding-top: 0; }

6. Pseudo-Element Selectors

Pseudo-elements create virtual elements or target specific parts of an element:

/* ::before and ::after — insert generated content */
.c-btn::before {
content: ''; /* Required — even for decorative elements */
display: block;
width: 1rem; height: 1rem;
background: currentColor;
}

/* Text-specific pseudo-elements */
p::first-line { font-weight: 600; } /* First rendered line */
p::first-letter { font-size: 3em; float: left; margin-right: 0.1em; }

/* Selection styling */
::selection { background: var(--color-primary); color: white; }
::-moz-selection { background: var(--color-primary); color: white; }

/* Placeholder text */
::placeholder { color: var(--color-text-subtle); font-style: italic; }

/* Scrollbar styling (Chrome/Edge) */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--color-surface); }
::-webkit-scrollbar-thumb { background: var(--color-border-strong); border-radius: 4px; }

7. Specificity Quick Reference

Selector Type Score Example
──────────────────────────────────────────────────────────────
Inline style 1,0,0,0 style="color:red"
#id 0,1,0,0 #header
.class, [attr], :pseudo-class 0,0,1,0 .btn, [disabled], :hover
element, ::pseudo-element 0,0,0,1 h1, ::before
*universal, combinators 0,0,0,0 *, >, +, ~

:is(), :not(), :has() Specificity of their most specific argument
:where() Always 0,0,0,0

Comparison: left-to-right → first difference wins
0,1,0,1 BEATS 0,0,3,0
(one ID beats any number of classes)

8. Selector Performance Tips

/* ✅ Fast: class selectors — the best balance of performance + reuse */
.c-card { }
.c-btn--primary { }

/* ✅ Fast: element selectors in reset context */
*, *::before, *::after { box-sizing: border-box; }

/* ⚠ Medium: attribute selectors (fine in practice, avoid on large lists) */
[data-theme="dark"] { }

/* ❌ Avoid: deep descendant selectors — browser reads right-to-left */
.l-sidebar .c-widget ul li a span { }
/* Browser: finds all <span>, checks if inside <a>, inside <li>... etc */

/* ✅ Better: flat BEM class */
.c-widget__link-label { } /* One class lookup — done */

/* ❌ Avoid in hot paths: :not() with complex selectors */
li:not(.special):not(.disabled):not(:last-child) { }

/* ✅ Better: add a class */
li.c-list__item--normal { }