Suggest an editImprove this articleRefine the answer for “CSS pseudo-classes and pseudo-elements”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CSS pseudo-classes** target whole elements by state or position (`:hover`, `:nth-child`); **pseudo-elements** style parts or inject virtual content (`::before`, `::first-line`). Single colon for pseudo-classes, double colon for pseudo-elements. ```css button:hover { background: blue; } /* pseudo-class: state */ .quote::before { content: '"'; } /* pseudo-element: injected content */ ``` **Key:** pseudo-elements create render-only boxes with no DOM node; pseudo-classes match real ones.Shown above the full answer for quick recall.Answer (EN)Image**CSS pseudo-classes and pseudo-elements** are two types of selectors with one key difference: pseudo-classes target whole elements based on state or position, while pseudo-elements style specific parts of elements or inject content that has no real node in the DOM. ## Theory ### TL;DR - Pseudo-classes use a single colon (`:hover`); pseudo-elements use a double colon (`::before`) - Pseudo-classes match real elements based on state or position; pseudo-elements create or target virtual sub-parts - Think of pseudo-classes as a mood ring on an element (reacts to what is happening); think of pseudo-elements as a sticky note attached to a specific spot - User interaction and DOM position: pseudo-class. Decoration or content injection: pseudo-element. - `::before` and `::after` require a `content` property, even if its value is empty ### Quick example ```css /* Pseudo-class: selects by position */ li:nth-child(odd) { background: #f0f0f0; } /* Pseudo-element: injects content before each li */ li::before { content: "→ "; } ``` ```html <ul> <li>Item 1</li> <!-- gray bg + arrow --> <li>Item 2</li> <!-- no bg + arrow --> <li>Item 3</li> <!-- gray bg + arrow --> </ul> ``` The pseudo-class changes the visual state based on DOM position. The pseudo-element adds virtual content that does not exist anywhere in the HTML source. ### Key difference Pseudo-classes respond to conditions outside the element: user interaction, DOM position, or element state (`:checked`, `:disabled`). Pseudo-elements create anonymous boxes in the browser's render tree. Those boxes exist in the layout engine but have no corresponding DOM node. That is why you cannot query `::before` with `document.querySelector` - there is nothing in the DOM to query. ### When to use - **Hover and focus states** on buttons and links: `:hover`, `:focus` - **Zebra striping** in lists or tables: `:nth-child(odd)`, `:nth-child(2n)` - **Form state styling**: `:checked`, `:disabled`, `:required` - **Decorative icons or counters** before or after elements: `::before`, `::after` - **Drop caps or styled first lines** in articles: `::first-letter`, `::first-line` - **Custom text selection color**: `::selection` If you find yourself adding an empty `<span>` just for decoration, that is a sign `::before` or `::after` will do the job with less markup. ### Comparison table | Aspect | Pseudo-classes | Pseudo-elements | |---|---|---| | **Notation** | Single colon `:hover` | Double colon `::before` | | **Targets** | Whole existing elements | Parts or virtual sub-elements | | **DOM presence** | Matches real nodes | Generates render-only boxes | | **Common examples** | `:hover`, `:nth-child(2n)`, `:not()`, `:has()` | `::before`, `::after`, `::first-line`, `::selection` | | **Queryable from JS** | Yes | No | | **When to use** | State-based or position-based selection | Decorative content, partial element styling | ### How the browser handles it When the style engine resolves selectors, pseudo-classes evaluate dynamic conditions against the computed style tree. `:hover` matches when a mouseenter event reaches the element. Pseudo-elements like `::before` generate anonymous inline boxes in the layout tree. These boxes inherit styles from the parent but have no real HTML node. Changing `::before` content from JavaScript is not directly possible, but a CSS custom property works as a bridge between JS and the pseudo-element. ### Common mistakes **Single colon for pseudo-elements:** ```css /* Old syntax - avoid */ p:before { content: ""; } /* Correct */ p::before { content: ""; } ``` Single colon still works in most browsers for backward compat, but it blurs the line between pseudo-class and pseudo-element syntax. **`:nth-child` counting the wrong siblings:** ```css .item:nth-child(2) { color: red; } ``` ```html <div><span class="item">1</span></div> <div class="item">2</div> <!-- NOT red: it is the 1st child of its own parent --> ``` `:nth-child` counts among siblings of the same parent, not among all `.item` elements in the DOM. When items sit in different containers, the counter resets per container. **Missing `content` on `::before` or `::after`:** ```css /* Nothing renders - content is required */ .icon::before { font-family: "Icons"; } /* Correct */ .icon::before { content: ""; font-family: "Icons"; } ``` **Removing `:focus` outline without a replacement:** ```css /* Breaks keyboard navigation */ button:focus { outline: none; } ``` Remove the outline only if you replace it with something equally visible. WCAG 2.1 AA requires visible focus indicators. A large portion of accessibility bugs I've seen in production codebases trace back to this exact line. ### Real-world usage - **Tailwind CSS**: `hover:`, `focus:` variant prefixes compile to `:hover`, `:focus` pseudo-classes - **Bootstrap 5**: `::before`/`::after` for chevron icons in accordions and dropdowns - **GitHub**: `:nth-child(odd)` for PR list zebra striping, `::before` for label decorations - **Material-UI**: `:checked` for switch toggle states, `::selection` for branded text highlight ### Follow-up questions **Q:** What is the difference between `:hover` in CSS and `mouseenter` in JavaScript? **A:** `:hover` bubbles through children, so hovering a nested element also marks the parent as hovered. `mouseenter` in JS fires only on the direct target with no bubbling. **Q:** Can you nest pseudo-elements, like `::before::after`? **A:** No. The spec does not allow pseudo-elements to have their own pseudo-elements. Use a real child element instead. **Q:** What is the specificity of `:not(.foo)`? **A:** `:not()` itself adds no specificity. Only the argument inside counts. So `:not(.foo)` has class-level specificity (0,1,0), the same as `.foo` alone. **Q:** How does `:has()` change the way you write pseudo-classes? **A:** `:has()` is a parent selector supported in Chrome 105+ and Safari 15.4+. `section:has(.warning) { border: 2px solid red; }` does what used to need JavaScript. It makes pseudo-classes genuinely reactive without any JS at all. **Q (senior):** What is the render performance difference between 100 `::before` elements and 100 extra `<span>` nodes? **A:** Pseudo-elements create extra boxes in the layout tree, which adds paint and layout cost. Real DOM nodes also carry cost, but browsers are heavily optimized for them. For large lists with `::before`, profile in the Chrome DevTools Layout panel. In most cases the difference is negligible, but at thousands of items, real nodes may win. ## Examples ### Zebra striping with priority icons ```css .issue { padding: 1rem; border-bottom: 1px solid #eee; } .issue:nth-child(odd) { background: #f6f8fa; } .issue.severity-high::before { content: "🔥 "; font-size: 1.2em; } ``` ```html <div class="issue severity-high">Fix login bug</div> <!-- gray + fire icon --> <div class="issue">Update docs</div> <!-- white, no icon --> <div class="issue severity-high">Security patch</div> <!-- gray + fire icon --> ``` `:nth-child(odd)` stripes rows by position. `::before` on a specific class adds decorative content only for high-priority items. No extra HTML elements needed for either effect. ### Scoped `:nth-child` in nested lists ```css /* Wrong: counts globally, breaks when you have multiple lists */ .row:nth-child(2n) { background: #f0f0f0; } /* Right: scoped per parent container */ .list .row:nth-child(2n) { background: #f0f0f0; } ``` ```html <div class="list"> <div class="row">A</div> <!-- no bg --> <div class="row">B</div> <!-- gray --> </div> <div class="list"> <div class="row">C</div> <!-- gray with scoped version, wrong without it --> </div> ``` Without the `.list` scope, `:nth-child` counts all sibling `.row` elements across the page. Adding the parent selector gives each list its own independent counter. This is a common source of striping bugs in React lists where components render into separate containers.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.