We are still cooking the magic in the way!
Pseudo-Elements & Generated Content
What Are Pseudo-Elements?
Pseudo-elements allow you to style specific parts of an element or to create virtual elements that do not exist in the HTML markup. While pseudo-classes (covered in the previous lesson) target elements in a particular state, pseudo-elements target sub-parts of elements -- like the first line of a paragraph, the first letter, or entirely new content inserted before or after an element.
Pseudo-elements use a double colon (::) syntax to distinguish them from pseudo-classes (which use a single colon). The general syntax is:
Pseudo-Element Syntax
selector::pseudo-element {
property: value;
}
/* Examples */
p::first-line { font-weight: bold; }
h1::first-letter { font-size: 2em; }
.quote::before { content: '\201C'; }
.quote::after { content: '\201D'; }
:before, :after, :first-line, :first-letter) from CSS2. However, the double-colon syntax (::before, ::after, ::first-line, ::first-letter) is the correct CSS3 standard and should always be used in new code.There is an important rule: you can only apply one pseudo-element per selector. You cannot chain two pseudo-elements like p::first-line::first-letter. However, you can combine a pseudo-element with pseudo-classes, such as a:hover::after.
::before and ::after -- The Workhorses
The ::before and ::after pseudo-elements are by far the most commonly used pseudo-elements in CSS. They create virtual child elements that are inserted as the first child (for ::before) or the last child (for ::after) of the selected element. These virtual elements do not exist in the DOM -- they are purely visual and generated entirely by CSS.
The content Property -- Required
Both ::before and ::after require the content property to function. Without content, the pseudo-element will not render at all. Even if you want an empty pseudo-element (for purely decorative purposes), you must set content: '' (an empty string).
The content property accepts several types of values:
- Strings -- Text content enclosed in quotes:
content: 'Hello' - Empty string -- Used for decorative elements:
content: '' - attr() -- Inserts the value of an HTML attribute:
content: attr(data-label) - url() -- Inserts an image:
content: url(icon.svg) - counter() -- Inserts a CSS counter value:
content: counter(section) - open-quote / close-quote -- Inserts quotation marks based on the
quotesproperty - no-open-quote / no-close-quote -- Increments/decrements quote nesting without inserting characters
- Concatenation -- You can combine multiple values:
content: 'Chapter ' counter(chapter) ': '
Example: Different content Property Values
/* String content */
.required-field::after {
content: ' *';
color: #e74c3c;
font-weight: bold;
}
/* Empty string for decorative elements */
.divider::after {
content: '';
display: block;
width: 60px;
height: 3px;
background-color: #3498db;
margin-top: 10px;
}
/* attr() -- display the href of links when printing */
@media print {
a[href]::after {
content: ' (' attr(href) ')';
font-size: 0.8em;
color: #666;
}
}
/* attr() -- tooltips from data attributes */
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.85rem;
white-space: nowrap;
}
/* url() -- insert an icon image */
.external-link::after {
content: url('data:image/svg+xml,...');
margin-left: 4px;
vertical-align: middle;
}
/* counter() -- automatic section numbering */
body {
counter-reset: section;
}
h2::before {
counter-increment: section;
content: 'Section ' counter(section) ': ';
color: #3498db;
font-weight: normal;
}
/* open-quote and close-quote */
blockquote {
quotes: '\201C' '\201D' '\2018' '\2019';
}
blockquote::before {
content: open-quote;
font-size: 2em;
color: #ccc;
line-height: 0;
vertical-align: -0.4em;
}
blockquote::after {
content: close-quote;
font-size: 2em;
color: #ccc;
line-height: 0;
vertical-align: -0.4em;
}
/* Concatenation -- combining multiple values */
.price::before {
content: '$';
}
.chapter::before {
counter-increment: chapter;
content: 'Ch. ' counter(chapter) ' -- ';
font-weight: bold;
}
::before and ::after is not part of the DOM and is not accessible to screen readers in a reliable way. Never use pseudo-elements to insert meaningful text content that is essential for understanding the page. Use them only for decorative or supplementary visual content. If the content is important, put it in the HTML.Decorative Use Cases for ::before and ::after
The most common use of ::before and ::after is to add decorative elements without cluttering the HTML. These pseudo-elements can be styled with any CSS property -- positioned absolutely, given backgrounds, borders, gradients, and even animations.
Example: Decorative Elements with ::before and ::after
/* Decorative line under headings */
h2 {
position: relative;
padding-bottom: 12px;
}
h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 3px;
background: linear-gradient(to right, #3498db, #2ecc71);
border-radius: 2px;
}
/* Quote icon before blockquotes */
blockquote {
position: relative;
padding-left: 40px;
}
blockquote::before {
content: '\201C';
position: absolute;
left: 0;
top: -10px;
font-size: 4em;
color: rgba(52, 152, 219, 0.3);
font-family: Georgia, serif;
line-height: 1;
}
/* Ribbon badge on cards */
.featured-card {
position: relative;
overflow: hidden;
}
.featured-card::before {
content: 'Featured';
position: absolute;
top: 15px;
right: -35px;
background-color: #e74c3c;
color: white;
padding: 4px 40px;
font-size: 0.75rem;
font-weight: bold;
transform: rotate(45deg);
text-transform: uppercase;
letter-spacing: 1px;
}
/* Overlay on hover for image cards */
.image-card {
position: relative;
overflow: hidden;
}
.image-card::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.7) 0%,
rgba(0, 0, 0, 0) 60%
);
opacity: 0;
transition: opacity 0.3s ease;
}
.image-card:hover::after {
opacity: 1;
}
/* Custom bullet points */
.custom-list li {
position: relative;
padding-left: 24px;
list-style: none;
}
.custom-list li::before {
content: '';
position: absolute;
left: 0;
top: 8px;
width: 8px;
height: 8px;
background-color: #3498db;
border-radius: 50%;
}
/* Step indicators */
.step {
position: relative;
padding-left: 50px;
}
.step::before {
content: attr(data-step);
position: absolute;
left: 0;
top: 0;
width: 36px;
height: 36px;
background-color: #3498db;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
The Clearfix Technique
One of the classic uses of ::after is the clearfix hack -- a technique to make a parent container properly contain its floated children. While modern CSS layout methods (Flexbox and Grid) have largely replaced the need for floats, you will still encounter clearfix in older codebases.
Example: The Classic Clearfix
/* The clearfix technique */
.clearfix::after {
content: '';
display: table;
clear: both;
}
/* Usage in HTML:
<div class="clearfix">
<div style="float: left; width: 50%;">Left column</div>
<div style="float: right; width: 50%;">Right column</div>
</div>
*/
/* Modern alternative: simply use overflow or display: flow-root */
.parent {
display: flow-root; /* Modern clearfix -- no pseudo-element needed */
}
display: flow-root on the parent achieves the same result as the clearfix without a pseudo-element.Icon Insertion with ::before and ::after
Pseudo-elements are frequently used to insert icons before or after text elements, especially when using icon fonts like Font Awesome or custom Unicode characters.
Example: Adding Icons via Pseudo-Elements
/* Unicode arrow icons */
.link-forward::after {
content: ' \2192'; /* Right arrow: → */
}
.link-back::before {
content: '\2190 '; /* Left arrow: ← */
}
.link-external::after {
content: ' \2197'; /* North-east arrow: ↗ */
font-size: 0.8em;
}
/* Checkbox icon */
.checkmark::before {
content: '\2713'; /* Check mark: ✓ */
color: #2ecc71;
margin-right: 8px;
font-weight: bold;
}
/* Warning icon */
.warning-text::before {
content: '\26A0'; /* Warning sign: ⚠ */
margin-right: 8px;
color: #f39c12;
}
/* Font Awesome icons (when using the font) */
.fa-icon::before {
font-family: 'Font Awesome 6 Free';
font-weight: 900;
margin-right: 8px;
}
.fa-icon.email::before {
content: '\f0e0'; /* Envelope icon */
}
.fa-icon.phone::before {
content: '\f095'; /* Phone icon */
}
::first-line -- Styling the First Line
The ::first-line pseudo-element targets the first rendered line of a block-level element. This is a dynamic selection -- if the browser window is resized and the text reflows, the styling automatically adjusts to whatever text now appears on the first line.
Only a limited set of CSS properties can be applied to ::first-line:
- Font properties:
font-family,font-size,font-weight,font-style,font-variant - Color and background properties
text-decoration,text-transform,letter-spacing,word-spacingline-heightvertical-align
Example: First Line Styling for Articles
/* Magazine-style first line */
article p:first-of-type::first-line {
font-weight: 700;
font-size: 1.1em;
color: #2c3e50;
text-transform: uppercase;
letter-spacing: 1px;
}
/* First line with a different color */
.lead-paragraph::first-line {
color: #3498db;
font-weight: 600;
}
/* News article opening line */
.news-body p:first-child::first-line {
font-variant: small-caps;
font-size: 1.15em;
}
::first-letter -- Drop Caps and Initial Letters
The ::first-letter pseudo-element targets the very first letter of a block-level element. This is the classic way to create drop caps -- the large decorative initial letters you see in books, magazines, and editorial websites.
Like ::first-line, ::first-letter accepts a limited set of properties, but it also supports margin, padding, border, and float -- which is what makes drop caps possible.
Example: Drop Caps and Decorative First Letters
/* Classic drop cap */
article p:first-of-type::first-letter {
font-size: 3.5em;
font-weight: 700;
float: left;
line-height: 0.8;
margin-right: 8px;
margin-top: 4px;
color: #2c3e50;
font-family: Georgia, 'Times New Roman', serif;
}
/* Colored drop cap with background */
.fancy-article p:first-child::first-letter {
font-size: 4em;
float: left;
line-height: 0.7;
margin-right: 12px;
margin-top: 6px;
padding: 8px 12px;
background-color: #3498db;
color: white;
border-radius: 4px;
font-weight: bold;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
/* Drop cap with decorative border */
.editorial p:first-of-type::first-letter {
font-size: 3em;
float: left;
margin-right: 10px;
margin-top: 2px;
line-height: 0.85;
border-bottom: 3px solid #e74c3c;
padding-bottom: 4px;
color: #e74c3c;
font-family: 'Playfair Display', serif;
}
/* Simple enlarged first letter (no float) */
.intro-text::first-letter {
font-size: 1.5em;
font-weight: 700;
color: #3498db;
}
"Hello, then ::first-letter applies to both " and H.float: left, a large font-size, and a carefully tuned line-height and margin. The line-height should be less than 1 (typically 0.7 to 0.85) to prevent the letter from pushing the second line too far down. Test across different browsers and font sizes, as drop cap rendering can vary.::selection -- Custom Text Highlighting
The ::selection pseudo-element styles the portion of text that the user has selected (highlighted) with their cursor. It allows you to customize the highlight color to match your site's design.
Only a few properties work with ::selection:
colorbackground-color(orbackground)text-decorationtext-shadow-webkit-text-stroke(in some browsers)
Example: Custom Selection Colors
/* Global selection color */
::selection {
background-color: #3498db;
color: white;
}
/* Firefox-specific (older versions) */
::-moz-selection {
background-color: #3498db;
color: white;
}
/* Different selection for code blocks */
pre::selection,
code::selection {
background-color: #2c3e50;
color: #2ecc71;
}
/* Brand-colored selection */
.brand-section ::selection {
background-color: #f39c12;
color: #2c3e50;
}
/* Subtle selection */
.minimal ::selection {
background-color: rgba(52, 152, 219, 0.2);
color: inherit;
}
::selection, always ensure sufficient contrast between the background color and the text color. A selection that is hard to read defeats its purpose. Test with both light and dark text content on your pages.::placeholder -- Form Input Placeholder Styling
The ::placeholder pseudo-element styles the placeholder text displayed in <input> and <textarea> elements when they are empty. By default, placeholder text is typically rendered in a lighter color, but you can customize it to match your design system.
Example: Styling Placeholder Text
/* Global placeholder styling */
::placeholder {
color: #95a5a6;
font-style: italic;
opacity: 1; /* Firefox defaults to lower opacity */
}
/* Specific input placeholder */
.search-input::placeholder {
color: #bdc3c7;
font-size: 0.95em;
}
/* Placeholder that changes on focus */
input:focus::placeholder {
color: transparent;
transition: color 0.3s ease;
}
/* Placeholder with icon (using special character) */
.search-box::placeholder {
color: #aaa;
}
/* Dark theme placeholder */
.dark-theme input::placeholder,
.dark-theme textarea::placeholder {
color: rgba(255, 255, 255, 0.4);
}
<label> elements for form fields, and use placeholders only for supplementary hints or examples.::marker -- List Bullet and Number Styling
The ::marker pseudo-element targets the bullet, number, or other marker of a list item (<li>). Before ::marker existed, customizing list bullets required workarounds with ::before and removing the default list-style. Now you can style the marker directly.
The properties that work with ::marker include:
color- All
fontproperties contentwhite-spaceanimationandtransitionpropertiesdirection,unicode-bidi,text-combine-upright
Example: Custom List Markers
/* Colored bullet points */
li::marker {
color: #3498db;
font-size: 1.2em;
}
/* Custom emoji markers */
.feature-list li::marker {
content: '\2713 '; /* Check mark */
color: #2ecc71;
}
.warning-list li::marker {
content: '\26A0 '; /* Warning sign */
color: #f39c12;
}
.info-list li::marker {
content: '\2139 '; /* Info symbol */
color: #3498db;
}
/* Numbered list with custom styling */
ol li::marker {
color: #e74c3c;
font-weight: bold;
font-size: 1.1em;
}
/* Animated marker on hover */
li:hover::marker {
color: #e74c3c;
transition: color 0.3s ease;
}
/* Different markers for nested lists */
ul ul li::marker {
content: '\2022 '; /* Small bullet */
color: #7f8c8d;
font-size: 0.8em;
}
CSS Counters in Depth
CSS counters are variables maintained by CSS that can be incremented, reset, and displayed using the content property. They are incredibly useful with ::before and ::after for creating automatic numbering systems without any JavaScript or HTML modification.
Example: Advanced CSS Counter Systems
/* Basic section numbering */
body {
counter-reset: section;
}
h2 {
counter-increment: section;
counter-reset: subsection;
}
h2::before {
content: counter(section) '. ';
color: #3498db;
}
/* Nested numbering (1.1, 1.2, 2.1, etc.) */
h3 {
counter-increment: subsection;
}
h3::before {
content: counter(section) '.' counter(subsection) ' ';
color: #7f8c8d;
}
/* Custom ordered list with counters */
.custom-ol {
list-style: none;
counter-reset: item;
padding-left: 0;
}
.custom-ol li {
counter-increment: item;
padding-left: 40px;
position: relative;
margin-bottom: 12px;
}
.custom-ol li::before {
content: counter(item, decimal-leading-zero);
position: absolute;
left: 0;
top: 0;
font-weight: bold;
color: #3498db;
font-size: 1.2em;
}
/* Step counter with circles */
.steps {
counter-reset: step;
}
.step-item {
counter-increment: step;
position: relative;
padding-left: 60px;
margin-bottom: 24px;
}
.step-item::before {
content: counter(step);
position: absolute;
left: 0;
top: 0;
width: 40px;
height: 40px;
background-color: #3498db;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
}
/* FAQ counter */
.faq {
counter-reset: question;
}
.faq-item {
counter-increment: question;
}
.faq-item .question::before {
content: 'Q' counter(question) ': ';
font-weight: bold;
color: #e74c3c;
}
counter() function accepts an optional second argument for the counter style. You can use any list-style-type value: counter(item, upper-roman) for Roman numerals (I, II, III), counter(item, lower-alpha) for letters (a, b, c), counter(item, decimal-leading-zero) for zero-padded numbers (01, 02, 03), and more.Combining Pseudo-Elements with Pseudo-Classes
One of the most powerful techniques in CSS is combining pseudo-elements with pseudo-classes. This allows you to create pseudo-element content that responds to user interaction, element state, or document structure.
Example: Interactive Pseudo-Elements
/* Show tooltip on hover using ::after */
.tooltip {
position: relative;
cursor: help;
}
.tooltip::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background-color: #2c3e50;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-size: 0.85rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
pointer-events: none;
z-index: 100;
}
.tooltip::before {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #2c3e50;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.tooltip:hover::after,
.tooltip:hover::before {
opacity: 1;
visibility: visible;
}
/* Animated underline on hover */
.fancy-link {
position: relative;
text-decoration: none;
color: #2c3e50;
}
.fancy-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background-color: #3498db;
transition: width 0.3s ease;
}
.fancy-link:hover::after {
width: 100%;
}
/* Button with sliding background on hover */
.slide-btn {
position: relative;
overflow: hidden;
z-index: 1;
background: transparent;
border: 2px solid #3498db;
color: #3498db;
padding: 12px 32px;
transition: color 0.4s ease;
}
.slide-btn::before {
content: '';
position: absolute;
inset: 0;
background-color: #3498db;
z-index: -1;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.4s ease;
}
.slide-btn:hover {
color: white;
}
.slide-btn:hover::before {
transform: scaleX(1);
transform-origin: left;
}
/* Focus ring using pseudo-elements */
.focus-ring {
position: relative;
}
.focus-ring::after {
content: '';
position: absolute;
inset: -4px;
border: 2px solid transparent;
border-radius: 8px;
transition: border-color 0.2s ease;
pointer-events: none;
}
.focus-ring:focus-visible::after {
border-color: #3498db;
}
/* Structural + pseudo-element: first article with drop cap */
article:first-of-type p:first-child::first-letter {
font-size: 3em;
float: left;
line-height: 0.8;
margin-right: 8px;
color: #2c3e50;
font-family: Georgia, serif;
}
/* nth-child + ::before for numbered steps */
.step-list li:nth-child(1)::before { content: '\2776'; }
.step-list li:nth-child(2)::before { content: '\2777'; }
.step-list li:nth-child(3)::before { content: '\2778'; }
.step-list li:nth-child(4)::before { content: '\2779'; }
.step-list li:nth-child(5)::before { content: '\277A'; }
Real-World Patterns and Techniques
Let us look at some complete, production-ready patterns that combine everything we have learned about pseudo-elements.
Example: Complete UI Patterns with Pseudo-Elements
/* 1. Breadcrumb separator */
.breadcrumb li + li::before {
content: '/';
color: #bdc3c7;
margin: 0 8px;
}
/* 2. Tag/badge component */
.tag {
display: inline-block;
padding: 4px 12px;
background-color: #eaf2f8;
color: #3498db;
border-radius: 4px;
font-size: 0.85rem;
position: relative;
}
.tag.removable::after {
content: '\00D7';
margin-left: 8px;
cursor: pointer;
font-weight: bold;
opacity: 0.6;
}
.tag.removable:hover::after {
opacity: 1;
color: #e74c3c;
}
/* 3. Card corner ribbon */
.ribbon-card {
position: relative;
overflow: hidden;
}
.ribbon-card::before {
content: 'NEW';
position: absolute;
top: 20px;
left: -30px;
width: 120px;
text-align: center;
padding: 4px 0;
background-color: #2ecc71;
color: white;
font-size: 0.7rem;
font-weight: bold;
letter-spacing: 2px;
transform: rotate(-45deg);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
/* 4. Divider with text */
.text-divider {
display: flex;
align-items: center;
text-align: center;
color: #7f8c8d;
font-size: 0.9rem;
}
.text-divider::before,
.text-divider::after {
content: '';
flex: 1;
height: 1px;
background-color: #ddd;
}
.text-divider::before {
margin-right: 16px;
}
.text-divider::after {
margin-left: 16px;
}
/* 5. Image aspect ratio container */
.aspect-ratio-16-9 {
position: relative;
width: 100%;
}
.aspect-ratio-16-9::before {
content: '';
display: block;
padding-top: 56.25%; /* 9/16 = 0.5625 */
}
.aspect-ratio-16-9 > * {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 6. Loading skeleton */
.skeleton {
position: relative;
overflow: hidden;
background-color: #e0e0e0;
border-radius: 4px;
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
Exercise 1: Magazine-Style Article Layout
Create a magazine-style article page using pseudo-elements with the following requirements:
- Add a large drop cap to the first letter of the first paragraph using
::first-letter(at least 3em, floated left, with a serif font and a contrasting color). - Style the first line of the opening paragraph with
::first-lineusing small-caps and bolder weight. - Add a decorative gradient line under every
<h2>heading using::afterwithposition: absolute. - Create a custom
::selectioncolor that matches a color palette of your choosing. - Add a large decorative opening quotation mark before every
<blockquote>using::before. - Use CSS counters to automatically number the
<h2>headings (Section 1, Section 2, etc.) via::before. - For any links in the article, add a small arrow icon after them using
::afterwith a Unicode character.
Challenge: Create a print stylesheet (@media print) that displays the full URL of links in parentheses after each link text using ::after with attr(href).
Exercise 2: Interactive UI Components with Pseudo-Elements
Build the following UI components using only CSS pseudo-elements (no extra HTML elements or JavaScript):
- Animated underline links: Create links where a line grows from left to right on hover using
::afterwithwidth: 0transitioning towidth: 100%. - Tooltip system: Using
[data-tooltip]attribute selectors combined with::afterand::before(for the arrow), create tooltips that appear above elements on hover. Include a smooth fade-in transition. - Custom checkboxes: Hide the default checkbox input and use
::beforeon the label to create a custom styled checkbox that shows a checkmark (Unicode \2713) when the input is:checked. - Progress indicator: Create a numbered step list where each step gets an automatic number via CSS counters and
::before, displayed in a colored circle. The current step (add a.activeclass) should have a different background color. - Tag removal button: Create a tag/badge component where
::afterdisplays an "x" symbol that changes color on hover, using.tag:hover::after.
Summary
In this lesson, you mastered CSS pseudo-elements and generated content. Here are the key takeaways:
- Pseudo-elements use the double colon syntax (
::) and target virtual sub-parts of elements or create new virtual elements. - ::before and ::after are the most used pseudo-elements. They require the
contentproperty and are perfect for decorative elements, icons, overlays, clearfix, and more. - The content property accepts strings,
attr(),url(),counter(),open-quote/close-quote, and combinations of these. - ::first-line styles the first rendered line dynamically and is great for editorial typography.
- ::first-letter enables drop caps and decorative initials -- a staple of magazine and editorial design.
- ::selection lets you customize the text highlight color to match your brand.
- ::placeholder styles form input placeholder text, but remember that placeholders are not label replacements.
- ::marker provides direct control over list bullet and number styling without workarounds.
- CSS counters (
counter-reset,counter-increment,counter()) enable automatic numbering systems with pseudo-elements. - Pseudo-elements can be combined with pseudo-classes (e.g.,
:hover::after) for interactive visual effects. - Always remember that pseudo-element content is not in the DOM and should not contain meaningful information that is essential for accessibility.