🎯

CSS Selectors

Every CSS selector type — basic, combinators, attribute, pseudo-classes, pseudo-elements and the new :is() :has() :where() family

Basic Selectors

Type, class, ID, universal and grouping

css·Type, class, ID and universal
/* Type selector — matches element name */
p { color: #333; }
h1, h2, h3 { font-weight: bold; }

/* Class selector — matches class attribute */
.card { border-radius: 8px; }
.btn.btn-primary { background: blue; } /* element must have BOTH classes */

/* ID selector — matches unique id attribute */
#navbar { position: sticky; top: 0; }

/* Universal selector — matches everything */
* { box-sizing: border-box; }
*.highlight { background: yellow; } /* same as .highlight */

/* Grouping — comma-separated list */
h1, h2, h3,
.title,
#hero-heading {
  font-family: "Inter", sans-serif;
}

Combinators

Descendant, child, adjacent sibling and general sibling

css·Descendant combinator (space)
/* Matches any p inside .card, no matter how deeply nested */
.card p { margin-bottom: 8px; }

/* Any em inside a blockquote */
blockquote em { font-style: normal; font-weight: bold; }
css·Child combinator >
/* Matches p that is a DIRECT child of .card only */
.card > p { margin-bottom: 8px; }

/* Direct li children of ul only — not nested lists */
ul > li { list-style: disc; }

/* Menu items at the top level only */
nav > ul > li > a { font-weight: 600; }
css·Adjacent sibling combinator +
/* Matches the FIRST p immediately after an h2 */
h2 + p { font-size: 1.1em; color: #555; }

/* Remove margin-top from first element after a heading */
h1 + *, h2 + *, h3 + * { margin-top: 0; }

/* Label immediately after a checkbox */
input[type="checkbox"] + label { cursor: pointer; }
css·General sibling combinator ~
/* Matches ALL p elements that are siblings after an h2 */
h2 ~ p { color: #444; }

/* All siblings after a :checked checkbox */
input:checked ~ .drawer { display: block; }

/* All .item siblings after .item--active */
.item--active ~ .item { opacity: 0.5; }

Attribute Selectors

Target elements by their HTML attributes and values

css·Presence and exact match
/* [attr] — element has the attribute (any value) */
a[href]       { color: blue; }        /* links with an href */
input[required] { border-color: red; } /* required inputs */
button[disabled] { opacity: 0.5; }

/* [attr="value"] — exact match */
input[type="text"]     { border: 1px solid #ccc; }
input[type="checkbox"] { width: 16px; height: 16px; }
a[rel="noopener"]      { text-decoration: underline; }
css·Substring matching
/* [attr^="value"] — starts with */
a[href^="https"] { /* secure links */ }
a[href^="mailto"] { /* email links */ }
a[href^="tel"]    { /* phone links */ }
[class^="icon-"]  { font-family: "Icons"; }

/* [attr$="value"] — ends with */
a[href$=".pdf"]  { /* PDF links */ }
a[href$=".zip"]  { /* download links */ }
img[src$=".svg"] { /* SVG images */ }

/* [attr*="value"] — contains anywhere */
a[href*="example.com"] { font-weight: bold; }
[class*="--modifier"]  { /* BEM modifiers */ }
css·List and hyphen matching
/* [attr~="value"] — value is in a space-separated list */
/* Useful for multi-value attributes like class or rel */
[rel~="noopener"] { }      /* matches rel="noopener noreferrer" */
[data-tags~="js"] { }      /* matches data-tags="css js html" */

/* [attr|="value"] — equals value OR starts with value- */
/* Most common use: language codes */
[lang|="en"] { font-family: serif; }  /* matches en, en-US, en-GB */
[lang|="zh"] { font-family: "Noto"; } /* matches zh, zh-CN, zh-TW */

/* Case-insensitive matching with i flag */
a[href$=".PDF" i] { }  /* matches .pdf .PDF .Pdf */

Pseudo-classes — State

User interaction and element state selectors

css·Link and user-action states
/* Link states — order matters: LVHA */
a:link    { color: blue; }           /* unvisited */
a:visited { color: purple; }         /* visited */
a:hover   { text-decoration: none; } /* mouse over */
a:active  { color: red; }            /* being clicked */

/* Keyboard focus */
:focus         { outline: 2px solid blue; }
:focus-visible { outline: 2px solid blue; } /* only for keyboard, not mouse */
:focus-within  { background: #f0f4ff; }     /* ancestor of focused element */

/* Interactive states */
button:hover    { background: #0056b3; }
button:active   { transform: scale(0.98); }
input:disabled  { opacity: 0.5; cursor: not-allowed; }
input:enabled   { cursor: text; }
input:read-only { background: #f5f5f5; }
css·Form and input states
/* Validity */
input:valid   { border-color: green; }
input:invalid { border-color: red; }

/* Only show validation after user interaction */
input:not(:focus):not(:placeholder-shown):invalid {
  border-color: red;
}

/* Required / optional */
input:required { border-left: 3px solid orange; }
input:optional { border-left: 3px solid #ccc; }

/* Checkbox / radio states */
input:checked           { accent-color: blue; }
input:indeterminate     { opacity: 0.7; }  /* partially checked */

/* Range and number */
input:in-range     { border-color: green; }
input:out-of-range { border-color: red; }

/* Placeholder shown */
input:placeholder-shown { font-style: italic; }

Pseudo-classes — Structural

Select elements by their position in the document tree

css·First, last and only child
/* Child position */
li:first-child  { font-weight: bold; }
li:last-child   { border-bottom: none; }
li:only-child   { margin: auto; }

/* Type-specific position */
p:first-of-type { font-size: 1.1em; }  /* first p among siblings */
p:last-of-type  { margin-bottom: 0; }
p:only-of-type  { text-align: center; }

/* Remove margin from last child — classic pattern */
.card > *:last-child  { margin-bottom: 0; }
.stack > * + *        { margin-top: 16px; } /* lobotomised owl */
css·nth-child and nth-of-type
/* :nth-child(An+B) — A=step, B=offset */

li:nth-child(1)    { }   /* first item */
li:nth-child(2)    { }   /* second item */
li:nth-child(-n+3) { }   /* first 3 items */
li:nth-child(n+4)  { }   /* from 4th item onwards */

/* Keywords */
li:nth-child(odd)  { background: #f9f9f9; }  /* 1,3,5,7... */
li:nth-child(even) { background: #ffffff; }  /* 2,4,6,8... */

/* Every 3rd, starting from 3 */
li:nth-child(3n)   { border-bottom: 2px solid; }

/* Every 3rd, starting from 1 (1,4,7,10...) */
li:nth-child(3n+1) { font-weight: bold; }

/* nth-of-type — counts only elements of matching type */
p:nth-of-type(2)      { color: blue; }  /* second p, ignores other siblings */
img:nth-of-type(odd)  { float: left; }

/* From the end */
li:nth-last-child(2)      { }  /* second to last */
li:nth-last-of-type(1)    { }  /* same as last-of-type */
css·Empty, root and scope
/* :empty — no children (including text nodes) */
td:empty    { background: #f5f5f5; }
p:empty     { display: none; }

/* :root — the document root (<html>) */
:root {
  --color-primary: #3b82f6;
  --spacing-unit: 8px;
  font-size: 16px;
}

/* :target — element whose ID matches the URL hash */
/* e.g. https://example.com/page#section-2 */
:target {
  scroll-margin-top: 80px;
  outline: 3px solid var(--color-primary);
}

/* :scope — the element being styled (useful in @scope) */
@scope (.card) {
  :scope { border-radius: 8px; }   /* the .card itself */
  p { margin: 0; }                  /* p inside .card only */
}

Modern Pseudo-classes

:is(), :not(), :has(), :where() and :any-link

css·:is() — match any in a list
/* :is() takes a selector list — specificity is the highest in the list */

/* Without :is() */
header h1, header h2, header h3,
footer h1, footer h2, footer h3,
main   h1, main   h2, main   h3 { color: navy; }

/* With :is() — much shorter */
:is(header, footer, main) :is(h1, h2, h3) { color: navy; }

/* Forgiving — invalid selectors are ignored, rest still work */
:is(h1, h2, :unsupported-selector, h3) { font-weight: bold; }

/* Nest heading styles cleanly */
:is(h1, h2, h3, h4) {
  font-family: "Inter", sans-serif;
  line-height: 1.2;
}
css·:not() — exclude elements
/* Single argument */
a:not([href])     { color: gray; }     /* anchor without href */
li:not(:last-child) { border-bottom: 1px solid #eee; }
input:not([type="submit"]) { border: 1px solid #ccc; }

/* Multiple arguments (CSS Selectors Level 4) */
p:not(.intro, .outro) { text-indent: 1.5em; }

/* Chain :not() calls for older browsers */
p:not(.intro):not(.outro) { text-indent: 1.5em; }

/* Exclude multiple elements from styling */
:is(h1,h2,h3,h4,h5,h6):not(.no-anchor)::before {
  content: "#";
  margin-right: 8px;
  opacity: 0.3;
}
css·:has() — parent and relational selector
/* :has() styles an element based on what it CONTAINS */
/* Often called the "parent selector" CSS never had */

/* Card that contains an image gets different padding */
.card:has(img) { padding: 0; }
.card:has(> img:first-child) { border-radius: 8px 8px 0 0; }

/* Form groups with errors */
.form-group:has(input:invalid) label { color: red; }
.form-group:has(input:invalid)::after {
  content: "Required";
  color: red;
}

/* Navigation with active descendant */
nav li:has(> a[aria-current="page"]) { background: #f0f4ff; }

/* Figure without a caption */
figure:not(:has(figcaption)) img { border-radius: 8px; }

/* Select previous sibling (was impossible before :has) */
/* Style h2 when followed immediately by a table */
h2:has(+ table) { margin-bottom: 4px; }

/* Article with more than one paragraph */
article:has(p ~ p) { column-count: 2; }
css·:where() — zero-specificity grouping
/* :where() is identical to :is() but ALWAYS has 0 specificity */
/* Use it for base/reset styles that are easy to override */

/* Reset margins on common elements — easily overridden */
:where(h1, h2, h3, h4, h5, h6) { margin: 0; }
:where(p, ul, ol, figure)       { margin: 0; }
:where(a) { color: inherit; text-decoration: none; }

/* vs :is() which inherits the specificity of its most specific argument */
:is(#main, .content) p   { color: blue; } /* specificity: 1,0,1 */
:where(#main, .content) p { color: blue; } /* specificity: 0,0,1 — easy to override */

/* Theme baseline that components can always override */
:where(.btn) {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
css·:any-link, :local-link and :visited
/* :any-link — matches any a, area, or link with href */
/* Equivalent to :is(:link, :visited) */
:any-link { color: blue; text-decoration: underline; }
:any-link:hover { text-decoration: none; }

/* Useful when you want to style all links regardless of visited state */
:any-link { color: var(--color-link); }

/* :link — unvisited links only */
a:link { color: #0066cc; }

/* :visited — only visited links */
a:visited { color: #551a8b; }

Pseudo-elements

Style specific parts of elements with ::before, ::after and friends

css·::before and ::after
/* Insert generated content before/after element content */
/* Must have content property — even content: "" for decorative use */

/* Icon before links */
a[href^="https"]::before {
  content: "🔒 ";
}

/* Decorative separator between items */
li:not(:last-child)::after {
  content: " •";
  margin-left: 8px;
  color: #999;
}

/* Quote marks */
blockquote::before { content: open-quote; font-size: 3em; line-height: 0; }
blockquote::after  { content: close-quote; }

/* Clearfix */
.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

/* Overlay using ::before */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.4);
}
css·::placeholder, ::selection and ::marker
/* ::placeholder — style input placeholder text */
input::placeholder {
  color: #999;
  font-style: italic;
}

::placeholder { opacity: 1; } /* Firefox reduces opacity by default */

/* ::selection — text selected by user */
::selection {
  background: #3b82f6;
  color: white;
}

p::selection { background: #fef08a; color: #000; }

/* ::marker — list item bullet or number */
li::marker {
  color: #3b82f6;
  font-weight: bold;
}

/* Custom counter */
ol { counter-reset: steps; }
li { counter-increment: steps; }
li::marker { content: "Step " counter(steps) ": "; }
css·::first-line, ::first-letter and ::backdrop
/* ::first-line — style the first rendered line of a block */
p::first-line {
  font-variant: small-caps;
  letter-spacing: 0.05em;
}

/* ::first-letter — style the first letter (drop cap) */
p:first-of-type::first-letter {
  font-size: 3em;
  font-weight: bold;
  float: left;
  line-height: 0.8;
  margin-right: 8px;
}

/* ::backdrop — full-screen backdrop behind dialog/<details> */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

/* ::file-selector-button — the button inside <input type="file"> */
input[type="file"]::file-selector-button {
  padding: 4px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}

Specificity

How browsers decide which rule wins when selectors conflict

css·Specificity scoring (A, B, C)
/*
  Specificity is a 3-part score: (A, B, C)

  A — ID selectors              #nav          = (1,0,0)
  B — class, attribute, pseudo-class
      .card  [type]  :hover     each          = (0,1,0)
  C — type and pseudo-element
      div  p  ::before          each          = (0,0,1)

  Universal * and combinators (> + ~ space) contribute 0.
  :is() :not() :has() take specificity of their most specific argument.
  :where() always contributes (0,0,0).

  Examples:
  h1                           = (0,0,1)
  .card                        = (0,1,0)
  .card h1                     = (0,1,1)
  #navbar                      = (1,0,0)
  #navbar .link:hover          = (1,1,1)
  style="" attribute           = (1,0,0,0) — inline style, separate layer
  !important                   overrides everything (avoid)
*/
css·Cascade layers — @layer
/* @layer gives you explicit cascade control without fighting specificity */

/* Declare layer order — later layers win */
@layer reset, base, components, utilities;

@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer base {
  a { color: blue; }           /* lower priority than components */
}

@layer components {
  .btn { padding: 8px 16px; }  /* wins over base, loses to utilities */
}

@layer utilities {
  .mt-4 { margin-top: 16px !important; } /* highest priority layer */
}

/* Unlayered styles beat ALL layers */
a { color: red; } /* this wins over @layer base a { color: blue; } */
css·Specificity tips and tricks
/* Bump specificity without adding meaning */

/* Repeat class — unusual but valid */
.card.card { color: blue; } /* (0,2,0) — beats .card */

/* :is() trick to match current element with higher specificity */
:is(#root) .card { }   /* (1,1,0) — the #root raises specificity */

/* Use :where() to write reusable low-specificity base styles */
:where(.btn) { padding: 8px; }  /* (0,0,0) — any rule beats this */

/* Avoid !important — use it only to override 3rd party styles */
/* If you need !important, use @layer instead */

/* Debugging: which rule is winning? */
/* Open DevTools → Elements → Computed → click the property */