Pseudo-classes and pseudo-elements
- A pseudo-class (one colon) selects elements in a particular state or position:
a:hover,li:first-child. - A pseudo-element (two colons) styles a part of an element, or adds one:
p::first-line,.note::before.
User actions
HTML
<style>
body { font-family: system-ui; }
a { color: #5a48f5; }
a:hover { color: #e0446a; }
button { margin: 4px; padding: 8px 14px; border: 2px solid #5a48f5; border-radius: 8px; background: white; font: 15px system-ui; }
button:hover { background: #f4f2ff; }
button:active { transform: translateY(1px); background: #e4e1ff; }
button:focus-visible { outline: 3px solid #0aa9cf; outline-offset: 2px; }
input { padding: 6px 8px; border: 2px solid #ccc; border-radius: 6px; }
input:focus { outline: none; border-color: #5a48f5; box-shadow: 0 0 0 3px rgba(90, 72, 245, .25); }
</style>
<p><a href="#">Hover over this link</a></p>
<button>Hover, press, or Tab to me</button>
<p><input placeholder="Click to focus"></p>:active applies while the button is pressed, and :focus-visible when it was reached by keyboard. The input replaces the default outline with a ring of its own, which is fine; removing it with nothing in its place is not.
Position among siblings
HTML
<style>
body { font-family: system-ui; }
table { width: 100%; max-width: 420px; border-collapse: collapse; }
th, td { padding: 8px 10px; text-align: left; }
thead th { background: #11151f; color: white; }
tbody tr:nth-child(odd) { background: #f4f2ff; }
tbody tr:hover { background: #d9f7ea; }
td:last-child { text-align: right; }
</style>
<table>
<thead><tr><th>Course</th><th>Lessons</th></tr></thead>
<tbody>
<tr><td>Python</td><td>64</td></tr>
<tr><td>JavaScript</td><td>31</td></tr>
<tr><td>SQL</td><td>23</td></tr>
<tr><td>HTML</td><td>17</td></tr>
<tr><td>AZ-900</td><td>30</td></tr>
</tbody>
</table>| Selector | Matches |
|---|---|
:first-child, :last-child | the first or last child of its parent |
:nth-child(odd), :nth-child(even) | every other child |
:nth-child(3) | the third child |
:nth-child(3n) | every third child |
:nth-child(-n + 3) | the first three children |
:only-child | a child with no siblings |
Form states
HTML
<style>
body { font-family: system-ui; }
.opt { position: relative; display: inline-block; margin: 4px; }
.opt input { position: absolute; opacity: 0; }
.opt span { display: inline-block; padding: 8px 14px; border: 2px solid #ccc; border-radius: 999px; cursor: pointer; }
.opt input:checked + span { border-color: #5a48f5; background: #5a48f5; color: white; }
.opt input:focus-visible + span { outline: 3px solid #0aa9cf; outline-offset: 2px; }
button:disabled { opacity: .5; cursor: not-allowed; }
</style>
<p>Pick a level:</p>
<label class="opt"><input type="radio" name="level" checked><span>Beginner</span></label>
<label class="opt"><input type="radio" name="level"><span>Intermediate</span></label>
<label class="opt"><input type="radio" name="level"><span>Advanced</span></label>
<p><button disabled>Disabled button</button></p>:checked together with + (the element immediately after) styles custom radio buttons, while the real input stays in the page for keyboards and screen readers. Other form states include :disabled, :required, :valid, :invalid and :placeholder-shown.
:not() and :has()
HTML
<style>
body { font-family: system-ui; }
.tags span:not(:last-child)::after { content: " ·"; color: #999; }
.plans { display: flex; gap: 10px; margin-top: 14px; }
.plan { width: 140px; padding: 12px; border: 2px solid #ddd; border-radius: 10px; }
.plan:has(input:checked) { border-color: #5a48f5; background: #f4f2ff; }
</style>
<p class="tags"><span>Python</span><span>SQL</span><span>React</span></p>
<div class="plans">
<label class="plan"><input type="checkbox"> Monthly</label>
<label class="plan"><input type="checkbox" checked> Yearly</label>
</div>:not(:last-child)puts a separator after every tag except the last one.:has()selects an element by what's inside it: here, a plan whose checkbox is ticked. It's often called the parent selector, and every modern browser supports it.
::before and ::after
::before and ::after insert decorative content at the start or end of an element. They need a content property, even an empty one:
HTML
<style>
body { font-family: system-ui; }
.quote { position: relative; max-width: 380px; padding: 16px 16px 16px 46px; border-radius: 10px; background: #f4f2ff; }
.quote::before { content: "“"; position: absolute; left: 10px; top: -6px; font: 700 60px Georgia, serif; color: #5a48f5; }
.new::after { content: "NEW"; margin-left: 8px; padding: 2px 6px; border-radius: 4px; vertical-align: middle;
background: #e0446a; color: white; font-size: 11px; font-weight: 700; }
a[href^="http"]::after { content: " ↗"; }
</style>
<p class="quote">Programs must be written for people to read, and only incidentally for machines to execute. (Harold Abelson)</p>
<p><span class="new">Container queries</span></p>
<p><a href="https://developer.mozilla.org">MDN Web Docs</a></p>Content added this way is decoration: it can't be selected, and screen readers may skip it, so never put important information there.
More pseudo-elements
HTML
<style>
body { max-width: 460px; font-family: Georgia, serif; }
p::first-letter { float: left; margin: 4px 8px 0 0; font-size: 3.2em; line-height: .9; font-weight: 700; color: #5a48f5; }
p::first-line { font-variant: small-caps; }
::selection { background: #0aa9cf; color: white; }
li::marker { color: #e0446a; }
input::placeholder { color: #9aa1b2; font-style: italic; }
</style>
<p>Once upon a time, a beginner opened a code editor, typed a single line and pressed Run. The computer answered, and that was the start of everything. Select some of this text to see a custom highlight colour.</p>
<ul><li>A custom bullet colour</li><li>thanks to ::marker</li></ul>
<input placeholder="A styled placeholder">Finished this lesson?Mark it complete to earn +20 XP and move your progress on.