"A selector is a question CSS asks the DOM. 'Show me every element that matches this shape.' The answer is the set the rule applies to."
The Five Categories
Every CSS selector falls into one (or a combination) of five categories:
- Simple selectors — match by element type, class, ID, attribute, or everything.
- Pseudo-classes — match by state (
:hover,:checked) or position (:nth-child). - Pseudo-elements — match conceptual parts of an element (
::before,::placeholder). - Combinators — describe relationships between elements (space,
>,+,~). - Logical combinators — group selectors with logic (
:is(),:where(),:not(),:has()).
Simple Selectors
h1— every<h1>element. (type selector).btn— every element with classbtn. (class selector)#main— the element with idmain. (id selector — IDs must be unique per page)*— every element. (universal selector — useful in resets, expensive in selector matching)[type="email"]— every element with the matching attribute. (attribute selector)
Attribute Selectors: A Mini Query Language
Attribute selectors are more powerful than they look:
[disabled]— has the attribute (any value).[type="email"]— exact match.[class~="primary"]— class list contains the whole word.[lang|="en"]— value is exactlyenor starts withen-.[href^="https"]— starts with.[href$=".pdf"]— ends with.[href*="github.com"]— contains.[data-state="open" i]— theiflag makes it case-insensitive.
This is how you style external links differently from internal ones, or add an icon to every PDF link, with no class names at all.
Pseudo-Classes: State And Position
State pseudo-classes match the element's current state:
- User actions:
:hover,:focus,:focus-visible,:focus-within,:active - Form state:
:checked,:disabled,:required,:optional,:valid,:invalid,:placeholder-shown - Link state:
:link,:visited - Content state:
:empty(no children),:target(the URL fragment) - Negation:
:not(selector)— match elements that don't match the inner selector
Position pseudo-classes match by location among siblings:
:first-child,:last-child,:only-child:nth-child(n),:nth-child(odd),:nth-child(2n+1),:nth-child(3n):first-of-type,:nth-of-type(n)— like the above but only counting siblings of the same element type
Pseudo-Elements: Style Parts That Aren't Real Elements
Pseudo-elements style conceptual parts of an element. Notice the double colon:
::before,::after— generated content. Always paired withcontent: "...". Used for icons, decorative quotes, list markers, badges.::placeholder— input placeholder text styling.::selection— the highlighted text when the user selects.::first-letter,::first-line— typographic effects.::marker— the bullet/number on list items.::backdrop— the dimmed overlay behind<dialog>when open viashowModal().
Combinators: Element Relationships
A B— descendant. B anywhere inside A.article p= every<p>inside any<article>.A > B— direct child.ul > li= only the top-level<li>s, not nested ones.A + B— adjacent sibling.h2 + p= the<p>immediately following an<h2>.A ~ B— general sibling.h2 ~ p= every<p>after the<h2>at the same level.
Logical Selectors: The Modern Power Tools
:is(a, b, c)— matches if any inner selector matches.:is(h1, h2, h3) a= anchors inside h1, h2, or h3. Specificity = highest of the arguments.:where(a, b, c)— same matching, but specificity always 0,0,0,0. Magic for design system base styles.:not(a, b)— matches if no inner selector matches.li:not(:last-child)= every list item except the last.:has(selector)— the parent selector.article:has(img)= articles that contain an image.form:has(input:invalid)= forms with invalid inputs.
Selector Lists: Comma-Separated
h1, h2, h3 { font-family: sans-serif; } — apply the same rule to every selector in the list. Each is evaluated independently, then specificity is computed per match. (Note: in :is() the list is treated as one logical group; that's the difference.)
* { ... } is the slowest selector (every element matches; then each one is checked for its ancestors). Performance only matters at massive scale — for normal sites, write the clearest selector and let the browser optimize.Pippa's Note
.flex, .gap-4, .text-amber-500 — every one is a simple class selector. Tailwind avoided the cascade wars by giving every utility a (0,0,1,0) specificity and relying on source order. Knowing the selector zoo means you understand why Tailwind chose that strategy, not just that it works.