Why does adding a ::after rule with a content value to an <img> or an <input> usually render nothing?
answer
- not every box has children to generate into
- where does the interior come from
- external resource or browser widget
- the selector matches, the box never appears
- wrap it and style the wrapper
basics
~20 sThose 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.
solid answer
~50 s`::before` and `::after` generate boxes *inside* the originating element, as its first and last children. A replaced element — `<img>`, `<input>`, `<br>`, `<iframe>`, `<video>` — has no child box area to generate into: its content box is painted from an external resource or a browser-supplied widget, so the generated boxes have nowhere to live and browsers do not render them. The workaround is structural: wrap the replaced element in a plain container and put the pseudo-element on the container, or use `background-image` on a nearby box. There is one well-known exception — several browsers *do* render `img::after` when the image fails to load, because the box then falls back to rendering alt text; that is the basis of the broken-image-fallback trick, and it is browser-dependent rather than guaranteed. Non-replaced elements like `<button>` and `<a>` support generated content normally.
code
css · 15 lines/* does nothing: <img> is replaced */
.avatar::after { content: ""; }
/* works: the wrapper is non-replaced */
.avatar-wrap { position: relative; display: inline-block; }
.avatar-wrap::after {
content: "";
position: absolute;
inset-block-end: 0;
inset-inline-end: 0;
width: 10px;
height: 10px;
border-radius: 50%;
background: green;
}go deeper
Recall the practical rule: generated content does not show up on images or form inputs. Wrap the element in a container and attach the pseudo-element there instead.
Explain the mechanism — ::before and ::after are generated as child boxes, and a replaced element's interior comes from an external resource, so there is no child box list to insert into.
Show the debugging path when a pseudo-element silently vanishes, and note that the selector matches while box generation fails, so devtools gives you no warning. Mention the broken-image fallback as browser-dependent.
Set the component convention: decoration hangs off a wrapper the design system owns, never off a browser-drawn control, so behaviour does not vary by engine or by control type across the product.
## Replaced versus non-replaced CSS divides elements into two kinds by where the content of their box comes from. A **non-replaced** element's box is filled by its children — text runs and other boxes that CSS lays out. A `<div>`, a `<p>`, a `<span>`, a `<button>` and an `<a>` are all non-replaced. A **replaced** element's box is filled by something CSS does not lay out: an external resource, or a widget the browser draws itself. `<img>`, `<video>`, `<iframe>`, `<embed>`, `<object>`, `<canvas>` and most `<input>` types are replaced. CSS sizes and positions the box, but the interior is out of its hands. ## Why that kills generated content `::before` and `::after` are defined as generated children of the originating element — `::before` is inserted as its first child box, `::after` as its last. On a replaced element there is no child box list to insert into: the interior is the replaced content, full stop. So the pseudo-element is not rendered. ```css /* renders nothing in practice */ img::after { content: "★"; } input::after { content: " *"; } br::before { content: "—"; } ``` It is worth being precise about the failure mode. The selector *matches* — the element is a perfectly valid subject for `img::after`. What fails is box generation. So there is no console warning, no invalid-rule diagnostic, no red flag in devtools beyond the pseudo-element simply not appearing in the element tree. ## Form controls are the messy case `<input>` is replaced, so the same reasoning applies, and browsers generally do not render generated content on it. `<textarea>` and `<select>` are edge cases where engines have historically disagreed, and browser behaviour has changed over time. The safe rule for production code is: **do not attach `::before`/`::after` to any form control that the browser draws for you.** If you need a decorative mark next to a checkbox, the classic pattern puts it on an associated non-replaced element instead: ```css /* generated content lives on the label, not the input */ .field { position: relative; } .field::after { content: ""; position: absolute; inset-inline-end: 8px; inset-block-start: 50%; width: 8px; height: 8px; background: crimson; } ``` Controls that are *not* replaced behave normally: `<button>` is a non-replaced element and takes `::before`/`::after` without any trouble, which is why icon-plus-label buttons are usually built that way. ## The broken-image exception Several browsers render `::before`/`::after` on an `<img>` when the image fails to load. The reasoning is that a failed image is no longer displaying replaced content — the box falls back to rendering the alt text as ordinary inline content, and generated boxes can then be laid out alongside it. That is the mechanism behind the popular broken-image styling trick: ```css img::after { content: "image unavailable"; display: block; } ``` Treat this as a progressive enhancement, not a contract: which browsers do it, and under exactly which failure conditions, is not something to build a required behaviour on. ## What to do instead Three reliable alternatives, in the order you should reach for them: 1. **Wrap it.** Put the replaced element inside a plain container box and attach the pseudo-element to the container. This is the standard fix and costs one extra box. 2. **Use a background.** `background-image` on a neighbouring box gives you decoration without any generated content at all, and backgrounds work on replaced elements' boxes. 3. **Style an adjacent non-replaced box.** For form controls, a wrapper or the control's label element is the usual home for the decorative mark. ## Debugging the symptom When a pseudo-element does not appear, work through the causes in order: is there a `content` declaration at all; is the box `display: inline` with a width you expected to apply; is the originating element replaced. The third is the one people miss, because everything about the CSS looks correct — the rule is valid, the selector matches, and nothing shows up.
- Does the selector img::after fail to match, or does it match and render nothing?It matches. `img` is a perfectly valid subject for the pseudo-element and the rule parses fine; what does not happen is box generation, because a replaced element's interior is not made of child boxes. That distinction matters when debugging: there is no invalid-selector warning to find, and the rule looks correct in devtools while producing no visible pseudo-element.
- Why does <button> accept ::before while <input type="button"> is unreliable?`<button>` is a non-replaced element — the browser lays out its children as ordinary boxes, so a generated first child fits naturally. `<input>` is replaced regardless of its type: the browser draws the widget, and there is no child box list to insert into. That is why icon buttons are built from `<button>` rather than from input-based controls.
saying these in an interview costs you the question
- Says the selector is invalid rather than the box ungenerated
- Assumes every element supports ::before and ::after
- Blames specificity when a pseudo-element does not render
- Relies on img::after fallback as guaranteed behaviour
- Thinks display: block on the pseudo-element will fix it