The cascade

Pseudo-classes and pseudo-elements

Style element states and generated parts with hover, focus-visible, first-child, before, and after selectors.

So far our selectors matched elements by what they are. Now we’ll match them by what state they are in, and we’ll style parts of an element that don’t exist in the HTML.

Pseudo-classes

A pseudo-class selects an existing element when it’s in a certain state or position. It starts with one colon:

a:hover { text-decoration-thickness: 3px; }
button:focus-visible { outline: 3px solid; }
li:first-child { font-weight: bold; }

:hover matches while the pointer is over the link. :focus-visible matches when the button has keyboard focus and the browser thinks a focus ring should show. :first-child matches a list item that is the first child of its parent.

There are many more: :last-child, :nth-child(2), :checked, :disabled, :not(.card). The pattern is always element, colon, condition.

Hover is a bonus, not a requirement

Be careful with :hover. Phones don’t hover. Keyboard users don’t hover. If a menu only appears on hover, a lot of people never see it. Use hover for feedback, and keep the action reachable another way.

When you tab through a page, the focused element needs a visible indicator, and :focus-visible is the hook for it. Never write outline: none without an equally visible replacement. You would be blinding keyboard users.

Match real state

Pseudo-classes read state from the HTML, so the HTML has to be honest. If you want a button to look disabled, add the disabled attribute and style button:disabled. Don’t add a .looks-disabled class while the button keeps working.

:focus-within follows the same idea: it matches a parent while any element inside it has focus. Handy for highlighting a whole form field group.

Pseudo-elements

A pseudo-element selects a part of an element, or creates a new part. It uses two colons:

.external-link::after {
  content: " ↗";
}

This adds a small arrow after every external link, without touching the HTML. ::before does the same at the start. The content property is required, even if it’s an empty string, or nothing is generated.

Other pseudo-elements select existing parts. ::first-line, ::first-letter, ::placeholder, ::marker for list bullets.

Keep meaning in the HTML

Generated content is for decoration. Screen readers don’t always announce it, you can’t copy it, and reader modes may drop it. If the arrow above meant “opens in a new tab” and nothing else said so, some users would never know. Put essential information in the document and use ::before and ::after for polish.

DevTools shows ::before and ::after as nodes in the Elements tree, even though they are not in your HTML, so they are easy to inspect.

Try the states on the course page without a mouse. Tab through the nav links and check that each one gets a visible focus ring. If anything on the page only works or only shows on hover, fix that now.

Lesson completed