CodeMIRAI
0 XP

Pseudo-classes and pseudo-elements

3 min read 6 runnable examples Quiz included
  • 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>
SelectorMatches
:first-child, :last-childthe 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-childa 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.