In CSS, why does a ::before rule render nothing unless it declares the content property, and what does content: "" actually produce?
answer
- nothing to style until something is generated
- the property that creates the box
- its initial value computes to none here
- empty string still makes a real box
- inline by default, so size needs display
basics
~20 sA ::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.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
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.
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.
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.
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