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
contentproperty instead of written in the HTML. - content
- The CSS property that gives
::beforeand::aftersomething 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::beforeor::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.
.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.
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.
label.required::after {
content: " *";
color: crimson;
}
abbr[title]::after {
content: " (" attr(title) ")";
}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.
.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.
input::placeholder {
color: #6b7280;
}
::selection {
background: #fde68a;
color: #111;
}
li::marker {
color: crimson;
}Try it yourself
- 1
Remove content
In the first example, delete
content: "New";. Predict: does the green badge still show with no text? (No: withoutcontentthere is no box at all.) Check the console line too. - 2
Make the bar a different shape
In the first example, change the
h2::afterwidth to100%and height to1px. You've made a full-width rule under the heading with no extra HTML. - 3
Change the marker
In the second example, change
.ticks li::markerto usecolor: seagreen, then addcontent: "✓ ";. Which browsers show the tick? Chrome and Firefox; Safari may keep the bullet.
Code & diagrams
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
Console
Nothing logged yet.Expected console output
new ::before content: "New"
plain ::before content: none
textContent: Fresh courseDrag the preview narrower: the small-caps words change, because `::first-line` follows the current layout.
Preview
Console
Nothing logged yet.The computed `content` is the attribute's value as a string: `attr()` is resolved separately for each element.
Preview
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>.
Break #2
Important text in generated content
Put a price or warning in CSS: .price::after { content: "Sale ends today"; }.
Break #3
Pseudo-element in the wrong place
Write a::after:hover { content: "→"; }.
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.
- ▸
::beforeand::afterare laid out as real boxes (inline by default), so they take part in Flexbox and Grid like children. A common trick iscontent: ""plusposition: absolutefor overlays, which you'll meet in Phase 6. - ▸
Quotes can come from CSS:
q::before { content: open-quote; }uses thequotesproperty, which browsers set per language, so<q>shows « » in French withlang="fr". - ▸
contentwith 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 fallbackcontentline 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
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, nevera::after:hover. - 2
::beforeand::aftercreate a generated box as the first and last child of the element. They only appear when thecontentproperty is set:content: "New"(text),content: ""(an empty box you can colour and size, for decoration), orcontent: attr(data-label)(the value of an attribute). Withoutcontent, 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
Generated content is not in the DOM (the page's tree as JavaScript sees it):
textContentdoesn't include it, and users usually can't select or copy it. But screen readers do read it, socontent: "→"may be read aloud as "right arrow". Use::before/::afterfor decoration and small labels, never for important text that belongs in the HTML. - 4
::first-letterstyles the first letter of a block (for drop caps) and::first-linethe 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-letteralso margins, padding, borders andfloat. - 5
::placeholderstyles the hint text in an empty input (<input placeholder="Email">).::selectionstyles text the user highlights with the mouse; only a few properties work (color,background-color,text-decoration,text-shadow).::markerstyles a list item's bullet or number:li::marker { color: crimson; }. It accepts colour and font properties, pluscontentin Chrome and Firefox, while Safari supports fewer properties. - 6
For specificity, a pseudo-element counts like a type selector (the lowest regular score), so
p::first-linescores likep p. Each element can have each pseudo-element only once: one::before, one::after.
Explain it without notes
What is the difference between a pseudo-class and a pseudo-element? Give two of each.
Why does ::before sometimes not appear at all? List the common causes.
Is generated content accessible? What should and shouldn't go in content?
What is the specificity of p::first-line compared with p:first-child?
Practice
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.
Make the first letter of every .lead paragraph twice the size and dark red.
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
::beforein the DevTools Elements tree). - ↔
::markeris the simplest way to colour bullets, but browser support for its properties varies; a::beforewithlist-style: nonegives full control at the cost of more CSS.
Done when you can
I can explain the difference between
:and::.I can add decoration with
::beforeand::afterand knowcontentis required.I can use
attr()insidecontent.I can style
::first-letter,::first-line,::placeholder,::selectionand::marker.I keep important text in the HTML, not in generated content.