Beyond the tag, the class and the id
Basic selectors (p, .notice, #header) get you out of trouble, but they fall
short the moment the design gets demanding: "style the first menu item", "only the
active link", "the even rows of the table". That's what CSS's more expressive
selectors are for: targeting an element by its position, its state or its
attributes, without touching a single line of HTML. And the best part: you see
every change instantly on screen.
Combinators: relationships between elements
A combinator relates two selectors according to the position of the elements in the document tree.
/* Descendant (space): any <a> inside a .nav, at any level */
.nav a { color: teal; }
/* Direct child (>): only the <li> that are immediate children of .menu */
.menu > li { font-weight: bold; }
/* Adjacent sibling (+): the <p> that comes RIGHT after an <h2> */
h2 + p { margin-top: 0; }
/* General sibling (~): ALL the <p> siblings that follow an <h2> */
h2 ~ p { color: #555; }
A BselectsBthat is insideA(at any depth).A > BrequiresBto be a direct child ofA.A + Bselects the first siblingBimmediately afterA.A ~ Bselects all theBsiblings that come afterA.
The trick to never mixing up > and a space: the space means "somewhere in
there", > means "the child itself, no grandchildren".
Pseudo-classes: selecting by state or position
A pseudo-class (a single :) targets an element according to a dynamic state
or its position among siblings.
a:hover { text-decoration: underline; } /* on mouse hover */
input:focus { outline: 2px solid royalblue; } /* when focused */
li:first-child { font-weight: bold; } /* the first child */
li:last-child { border-bottom: none; } /* the last child */
li:nth-child(2) { background: #eef; } /* the 2nd child */
li:nth-child(odd) { background: #f7f7f7; } /* odd ones: 1, 3, 5... */
p:not(.featured) { opacity: 0.7; } /* all except .featured */
:nth-child() accepts a number, the keywords odd/even, or an an+b
formula (for example 3n for one out of every three). It's your best friend for
those alternating-row tables, without tagging a single class by hand.
Pseudo-elements: styling generated parts
A pseudo-element (double ::) styles a specific part of the element or
generates content. ::before and ::after insert content and require the
content property.
.price::before { content: "€ "; color: green; }
.external-link::after { content: " ↗"; }
p::first-line { font-weight: bold; }
Without content, the ::before/::after pseudo-elements are not shown. This
is the number-one slip-up: the rule looks correct, but nothing appears on screen
because content is missing. Remember it and you'll save yourself a good chunk
of head-scratching.
Attribute selectors
They target elements according to the presence or value of an HTML attribute.
input[type="text"] { border: 1px solid #ccc; } /* exact value */
a[target] { font-weight: bold; } /* has the attribute */
a[href^="https"] { color: green; } /* starts with */
img[src$=".png"] { border: 1px solid #ddd; } /* ends with */
a[href*="example"] { color: orange; } /* contains */
By combining these selectors you can target almost any element with sniper-like precision, without cluttering the HTML with filler classes. Fewer invented classes, cleaner CSS.