skip to content

Pseudo-elements

Selectors that target rendered sub-parts of an element rather than a state — the generated ::before/::after boxes, the first line, the list marker, the input placeholder. Interviewers ask for the single-colon versus double-colon story and why content is mandatory.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

5

In CSS, why does a ::before rule render nothing unless it declares the content property, and what does content: "" actually produce?

level: juniorimportance: must knowfreq 78%

answer

  1. nothing to style until something is generated
  2. the property that creates the box
  3. its initial value computes to none here
  4. empty string still makes a real box
  5. inline by default, so size needs display

basics

~20 s

A ::before box exists only if the content property generates it — content's initial value computes to none on ::before and ::after, so without it no box is created at all. content: "" generates a real but empty inline box you can then style.

solid answer

~40 s

`::before` and `::after` are *generated* boxes: they are not part of the document, so the browser has to be told to create them. That instruction is the `content` property. Its initial value is `normal`, which computes to `none` on these two pseudo-elements, so a rule with only `color` or `width` in it produces nothing to style. Declaring `content: ""` creates an empty box with no text, which is the usual idiom for decorative shapes, icon overlays and gradient bars. The generated box is the element's first (or last) child in the box tree, it inherits inheritable properties from the originating element, and it defaults to `display: inline` — so if you want to give it a width or height you also need `display: block`, `display: inline-block`, or absolute positioning.

code

css · 17 lines
css
.required-field::after {
  content: " *";
  color: #b00020;
}

.divider::before {
  content: "";
  display: block;
  height: 1px;
  background: #ddd;
}

.tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  inset-block-start: 100%;
}

go deeper

for a junior

Remember the one-line rule: no content declaration, no box. Say that content: "" makes an empty box for decoration and that you usually add display: block or absolute positioning to size it.

for a middle

Explain the mechanism: content's initial value normal computes to none on ::before and ::after, so the box is never generated. Be ready to list the value types — strings, url(), attr(), counter(), quotes.

for a senior

Show where the generated box lands in the box tree: first or last child of the originating element, inheriting from it, and becoming a flex or grid item inside those containers. Point out that generated text is not selectable, copyable or searchable.

for a principal

Own the guidance for the codebase: generated content is for decoration and for counters, never for meaningful copy that must be localised, searched or read out. Decide where that line sits and make it reviewable.

## What a pseudo-element is Most selectors match elements that already exist in the document. A pseudo-element instead addresses a part of the rendering that has no element of its own. `::before` and `::after` are the extreme case: they address boxes that do not exist at all until CSS asks for them. That is why they are called *generated content*. Because the box is generated, CSS needs an explicit signal to generate it, and that signal is the `content` property. ## Why `content` is mandatory The `content` property's initial value is the keyword `normal`. On a regular element `normal` means "render your own children". On `::before` and `::after`, `normal` computes to `none`, and `none` means *no box is generated*. So this rule does absolutely nothing: ```css .card::before { color: red; width: 40px; height: 40px; background: black; } ``` There is no box for `color`, `width` or `background` to apply to. Add one line and the box appears: ```css .card::before { content: ""; display: block; width: 40px; height: 40px; background: black; } ``` Note that `content: none` and `content: normal` both suppress the box, and `content: initial` resolves to `normal`, so it suppresses it too. Only a value that actually names something to render — a string, an image, a counter, an attribute value, or a quote keyword — creates the box. ## What values `content` accepts - **A string**: `content: "→"`. Escapes use CSS syntax, so a Unicode code point is written `content: "\2192"`. - **The empty string**: `content: ""` — a box with no text, the workhorse for decoration. - **An image**: `content: url(check.svg)` or a gradient, e.g. `content: linear-gradient(red, blue)`. An image value makes the pseudo-element behave like a replaced box, so text styling no longer applies to it. - **`attr()`**: `content: attr(data-label)` pulls a string from an attribute on the originating element. - **Counters**: `content: counter(section) ". "`, used with `counter-reset` and `counter-increment` for numbered headings. - **Quote keywords**: `open-quote` / `close-quote`, which render the current `quotes` value. Values can be concatenated by juxtaposition: `content: "Step " counter(step) ": "`. ## Where the generated box sits `::before` is inserted as the first child of the originating element's box, `::after` as the last — *inside* the element, not next to it. Two consequences follow. First, it inherits inheritable properties (`color`, `font-family`, `line-height`) from the originating element, not from the parent. Second, it participates in that element's formatting context: inside a flex container the generated box becomes a flex item, inside a grid container it becomes a grid item and can be auto-placed into a track. ## The `display: inline` default A generated box defaults to `display: inline`. On an inline box, `width` and `height` do not apply and vertical margins do not affect layout, which is why beginners report that "width is ignored on ::before". Three standard fixes: ```css /* 1. make it a block-level box */ .bar::before { content: ""; display: block; width: 100%; height: 2px; } /* 2. keep it in the text flow but sizable */ .icon::before { content: ""; display: inline-block; width: 1em; height: 1em; } /* 3. take it out of flow and pin it to the element */ .badge { position: relative; } .badge::after { content: ""; position: absolute; inset: 0; } ``` Option 3 is the common overlay pattern: the originating element establishes a containing block with `position: relative`, and the absolutely positioned pseudo-element stretches across it. ## Practical notes A whitespace-only string such as `content: " "` is *not* the same as `content: ""` — it renders a space character, which may collapse or may add width depending on `white-space`. And because generated content lives only in the stylesheet, it cannot be selected, copied or found with the browser's in-page search, which is why it belongs to decoration rather than to meaningful copy.

  • You set width and height on a ::before that has content: "" and nothing changes. Why?
    A generated box defaults to `display: inline`, and `width`/`height` do not apply to non-replaced inline boxes. Give it `display: block` or `display: inline-block`, or position it absolutely — any of those makes it a box whose dimensions are honoured. An image value such as `content: url(x.svg)` also makes it sizable, because the box is then replaced.
  • Does a ::before box inherit from the element it is attached to, or from that element's parent?
    From the originating element. It is generated as that element's first child in the box tree, so inheritable properties — `color`, `font-family`, `font-size`, `line-height` — come from the element the selector matched. That is why an icon written with `content` picks up the button's font size automatically and scales with `em` units.
  • How does ::before behave when the originating element is a flex or grid container?
    It becomes a real flex item or grid item. In a flex container it sits before the element's other children on the main axis and responds to `flex-grow`, `flex-shrink` and `order`; in a grid container it is auto-placed like any other item and can be given an explicit `grid-column`. This surprises people who expect generated content to be inert decoration.

saying these in an interview costs you the question

  • Thinks ::before inserts an element into the DOM
  • Says content is optional and only sets text
  • Believes content: none and content: "" are the same
  • Expects width to work on a default inline pseudo-element
  • Thinks ::before renders outside the element, before it

context

open as a page

What is the difference between the CSS selectors :before and ::before, and which pseudo-elements refuse the single-colon form?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Both forms select the same generated box. Double colon is the modern syntax that distinguishes pseudo-elements from pseudo-classes; single colon survives only for the four CSS2 pseudo-elements (::before, ::after, ::first-line, ::first-letter). Anything newer requires two colons.

open as a page

Why does adding a ::after rule with a content value to an <img> or an <input> usually render nothing?

level: middleimportance: should knowfreq 46%

basics

~20 s

Those are replaced elements: their box is filled by an external resource or a browser-supplied widget rather than by child boxes, so there is nowhere for a generated child box to go. Wrap the element in a non-replaced one and style that instead.

open as a page

When is CSS generated content (a content value on ::before or ::after) the wrong place to put text, and what breaks if meaningful copy lives there?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Generated content is a rendering instruction, not document text: it cannot be selected, copied or found with in-page search, translation tools skip it, and how assistive technology announces it varies by engine. Keep it decorative and put meaningful copy in the document.

open as a page

In CSS, why do width, padding and border have no effect when set on ::selection or ::marker?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Because each pseudo-element specification defines its own restricted property set. ::selection only paints over existing text, so it accepts colour, background-color, text-decoration and text-shadow; ::marker accepts font properties, colour and content. Neither generates a box you can size.

open as a page