Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~26 min· topic 6 of 6

Topic 1.6

Pseudo-elements

In one line

Pseudo-elements start with two colons and style a part of an element that has no tag of its own: generated boxes before or after its content, its first letter or line, the placeholder, the selected text or a list marker.

Think of it like this

A birthday card. The card itself is the element. A sticker you add before the message and a signature after it are extra decorations that aren't part of the printed text (::before, ::after). Writing the first letter big and fancy, or the first line in bold, styles a part of the text that has no separate piece of paper of its own.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Pseudo-element
A selector part after two colons, like ::before, that styles a piece of an element or adds a box that isn't in the HTML.
Generated content
Text or boxes created by CSS with the content property instead of written in the HTML.
content
The CSS property that gives ::before and ::after something to show. Without it they don't appear.
Replaced element
An element whose look comes from outside the page, like <img> or <input>. It can't have ::before or ::after.
DOM
The tree of elements the browser builds from your HTML, which JavaScript reads and changes.
Drop cap
A big first letter that drops down beside the first few lines of a paragraph, as in old books.
Marker
The bullet or number at the start of a list item.
attr()
A CSS function that reads an attribute's value, used inside content.

Step by step

01Add a label before the text

Set content and the browser inserts a box as the element's first child. Style it like any inline element.

style.csswhole filecss
.new::before {
  content: "New";
  margin-right: 6px;
  padding: 1px 6px;
  border-radius: 4px;
  background: seagreen;
  color: white;
  font-size: 0.8em;
}

02Draw decoration with an empty box

content: "" creates an empty box. Make it a block, give it a size and a colour, and you have an underline, a bar or a shape with no extra HTML.

style.csswhole filecss
h2::after {
  content: "";
  display: block;
  width: 48px;
  height: 4px;
  margin-top: 6px;
  background: #2563eb;
}

03Show an attribute's value

attr() copies an attribute into the generated content. Here, every required field's label shows a red star from CSS alone.

style.csswhole filecss
label.required::after {
  content: " *";
  color: crimson;
}
abbr[title]::after {
  content: " (" attr(title) ")";
}
Show an attribute's valuediagram
Rendering diagram…

04Style the first letter and line

A drop cap is ::first-letter with a big font and float: left. ::first-line follows the layout: make the window narrower and fewer words are styled.

style.csswhole filecss
.story::first-letter {
  float: left;
  font-size: 3em;
  line-height: 1;
  margin-right: 6px;
  color: #7c3aed;
}
.story::first-line {
  font-variant: small-caps;
}

05Style placeholders, selections and markers

Placeholder text should stay readable: keep enough contrast (it is often too pale). Selection colours are a small brand touch. Markers let you colour bullets without images.

style.csswhole filecss
input::placeholder {
  color: #6b7280;
}
::selection {
  background: #fde68a;
  color: #111;
}
li::marker {
  color: crimson;
}

Try it yourself

  1. 1

    Remove content

    In the first example, delete content: "New";. Predict: does the green badge still show with no text? (No: without content there is no box at all.) Check the console line too.

  2. 2

    Make the bar a different shape

    In the first example, change the h2::after width to 100% and height to 1px. You've made a full-width rule under the heading with no extra HTML.

  3. 3

    Change the marker

    In the second example, change .ticks li::marker to use color: seagreen, then add content: "✓ ";. Which browsers show the tick? Chrome and Firefox; Safari may keep the bullet.

Code & diagrams

::before and ::after need content New tab

The badge shows on screen, but `textContent` doesn't include it: generated content is not part of the DOM. Without a `content` rule, the computed value is `none` and no box exists.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

new ::before content: "New"
plain ::before content: none
textContent: Fresh course
First letter, first line, placeholder, selection and markers New tab

Drag the preview narrower: the small-caps words change, because `::first-line` follows the current layout.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Labels from attributes with attr() New tab

The computed `content` is the attribute's value as a string: `attr()` is resolved separately for each element.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

"2 MB"

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

::before on an image

Write img::before { content: "Photo:"; } and add an <img>.

terminal
$ the preview shows
── what you'll see ──
The image appears, but no "Photo:" label.

Break #2

Important text in generated content

Put a price or warning in CSS: .price::after { content: "Sale ends today"; }.

terminal
$ try to copy the text / search the page
── what you'll see ──
The words can't be selected or copied, Ctrl+F may not find them, and they disappear if the CSS fails to load.

Break #3

Pseudo-element in the wrong place

Write a::after:hover { content: "→"; }.

terminal
$ the preview shows
── what you'll see ──
No arrow appears, on hover or otherwise.

Myth vs fact

Myth

:before and ::before are different things.

Fact

They are the same pseudo-element. The single colon is accepted for :before, :after, :first-line and :first-letter only for old pages; newer pseudo-elements require two colons.

Myth

Screen readers ignore content added by CSS.

Fact

Most screen readers read generated text. Decorative symbols may be announced; use alternative text (content: "★" / "") or keep them out of content.

Myth

You can add as many ::before boxes as you like.

Fact

Each element gets one ::before and one ::after. For more decoration, add an element or use backgrounds and borders.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    ::before and ::after are laid out as real boxes (inline by default), so they take part in Flexbox and Grid like children. A common trick is content: "" plus position: absolute for overlays, which you'll meet in Phase 6.

  • ▸

    Quotes can come from CSS: q::before { content: open-quote; } uses the quotes property, which browsers set per language, so <q> shows « » in French with lang="fr".

  • ▸

    content with an alternative text after a slash (content: "→" / "next") controls what assistive tech announces. It is supported in current Chrome, Safari and Firefox; older browsers ignore the whole declaration, so put a plain fallback content line before it.

  • ▸

    Other pseudo-elements you'll meet later: ::backdrop (behind dialogs and full-screen elements), ::file-selector-button (the button of <input type="file">) and ::part() for styling inside web components.

Remember this

  1. 1

    A pseudo-element is written with two colons after a selector: p::first-line. A pseudo-class (one colon) selects a whole element in a certain state; a pseudo-element selects a part of an element, or creates a box that doesn't exist in the HTML. Older pseudo-elements also accept one colon (:before) for backwards compatibility, but write two. A pseudo-element must come last in the selector: a:hover::after, never a::after:hover.

  2. 2

    ::before and ::after create a generated box as the first and last child of the element. They only appear when the content property is set: content: "New" (text), content: "" (an empty box you can colour and size, for decoration), or content: attr(data-label) (the value of an attribute). Without content, nothing is created. They don't work on replaced elements such as <img> and <input>, whose content comes from outside the page and has no inside to put children in.

  3. 3

    Generated content is not in the DOM (the page's tree as JavaScript sees it): textContent doesn't include it, and users usually can't select or copy it. But screen readers do read it, so content: "→" may be read aloud as "right arrow". Use ::before/::after for decoration and small labels, never for important text that belongs in the HTML.

  4. 4

    ::first-letter styles the first letter of a block (for drop caps) and ::first-line the first line as it is currently laid out, so the styled words change when the window is resized. Both only accept a limited set of properties: fonts, colours, backgrounds, and for ::first-letter also margins, padding, borders and float.

  5. 5

    ::placeholder styles the hint text in an empty input (<input placeholder="Email">). ::selection styles text the user highlights with the mouse; only a few properties work (color, background-color, text-decoration, text-shadow). ::marker styles a list item's bullet or number: li::marker { color: crimson; }. It accepts colour and font properties, plus content in Chrome and Firefox, while Safari supports fewer properties.

  6. 6

    For specificity, a pseudo-element counts like a type selector (the lowest regular score), so p::first-line scores like p p. Each element can have each pseudo-element only once: one ::before, one ::after.

Explain it without notes

01

What is the difference between a pseudo-class and a pseudo-element? Give two of each.

02

Why does ::before sometimes not appear at all? List the common causes.

03

Is generated content accessible? What should and shouldn't go in content?

04

What is the specificity of p::first-line compared with p:first-child?

Practice

01

Add a grey / separator between breadcrumb links in <nav class="crumbs"><a>Home</a><a>CSS</a><a>Selectors</a></nav>, but not after the last one.

02

Make the first letter of every .lead paragraph twice the size and dark red.

03

Show the URL of every link in printed-style text after it, using attr().

Trade-offs

  • ↔

    Pseudo-elements add decoration without extra HTML, keeping markup clean, but the decoration is invisible to JavaScript and harder to find when debugging (look for ::before in the DevTools Elements tree).

  • ↔

    ::marker is the simplest way to colour bullets, but browser support for its properties varies; a ::before with list-style: none gives full control at the cost of more CSS.

Done when you can

  • I can explain the difference between : and ::.

  • I can add decoration with ::before and ::after and know content is required.

  • I can use attr() inside content.

  • I can style ::first-letter, ::first-line, ::placeholder, ::selection and ::marker.

  • I keep important text in the HTML, not in generated content.