Imagine you are a newspaper editor. You want the very first letter of every article to be huge, bold, and decorative · like the letter "T" in old books that takes up four lines. You could wrap that letter in a <span> tag manually in every article. But what if you have hundreds of articles? There must be a smarter way.
That smarter way is CSS pseudo-elements.
A pseudo-element lets you target and style a specific part of an element · the first letter, the first line, virtual content injected before or after the element, list markers, or highlighted selected text · all without touching your HTML.
In this lesson you will learn:
- What a pseudo-element is and why it is different from a pseudo-class
- The
::double-colon notation and why it matters - All major pseudo-elements:
::first-letter,::first-line,::before,::after,::marker,::selection - The powerful
contentproperty and all its values - How to combine pseudo-elements with other selectors
- Real-world professional use cases
- A complete mini-project: a styled article page that uses pseudo-elements throughout
Before continuing, make sure you understand these ideas. Short explanations are provided.
What Is a CSS Selector?
A selector is the part of a CSS rule that tells the browser which HTML element to style.
p { /* ← this is the selector — targets all <p> elements */
color: blue;
}What Is a CSS Property and Value?
Inside the curly braces {}, you write property-value pairs:
p {
color: blue; /* property: color, value: blue */
font-size: 18px; /* property: font-size, value: 18px */
}What Is a Pseudo-class? (Brief Reminder)
A pseudo-class targets an element in a specific state. It uses a single colon :.
a:hover { /* Styles a link ONLY when the mouse hovers over it */
color: red;
}What Is a Pseudo-element? (Preview)
A pseudo-element targets a specific part of an element · not the whole element, but a piece of it. It uses a double colon ::.
p::first-letter { /* Styles ONLY the very first letter of a paragraph */
font-size: 3em;
color: crimson;
}The difference in a nutshell:
- Pseudo-class = targets an element in a state (
:hover,:focus,:nth-child) - Pseudo-element = targets a part of an element (
::first-letter,::before,::after)
The Big Idea
Every HTML element renders content on screen. Most of the time, that content is exactly what you wrote in your HTML file. But sometimes you want to:
- Style just the first letter of a paragraph differently
- Style just the first visible line of text
- Insert a decorative icon or label before some text · without adding HTML
- Insert extra text or a symbol after a link
- Change how a bullet point looks in a list
- Change the background colour when a user highlights text
All of these are jobs for pseudo-elements. They are like invisible "virtual" elements that CSS creates and attaches to your real elements.
Real-world analogy: Think of a pseudo-element like a sticky note you place on a book. The book itself (your HTML) is unchanged · the sticky note is separate from the original content but is visually present and styled.
The Double-Colon Syntax
All pseudo-elements use a double colon :: before their name:
selector::pseudo-element {
css-property: value;
}Examples:
p::first-letter { ... }
p::first-line { ... }
p::before { ... }
p::after { ... }
li::marker { ... }
::selection { ... }Historical note: In CSS1 and CSS2, a single colon
:was used for both pseudo-classes and pseudo-elements. In CSS3, the W3C introduced the double colon::specifically for pseudo-elements, to clearly distinguish them from pseudo-classes. For backward compatibility, browsers still accept the old single-colon syntax for the four original pseudo-elements (::first-letter,::first-line,::before,::after). However, you should always write the modern double-colon notation.
Quick Reference Table
| Pseudo-element | What It Targets | Category |
|---|---|---|
::first-letter | The very first letter of a block of text | Text |
::first-line | The first visible line of a block of text | Text |
::before | A virtual element inserted before the content | Content |
::after | A virtual element inserted after the content | Content |
::marker | The bullet or number of a list item | Content |
::selection | The text highlighted/selected by the user | Content |
Text pseudo-elements target specific portions of existing text content.
::first-letter · Style Just the First Letter
What Is It?
::first-letter targets the very first letter (or character) of a block-level element's text content. It is most famous for creating drop caps · a typographic technique where the first letter of an article is much larger than the rest of the text.
You have seen this in novels, magazines, and newspaper articles. It signals the start of an important section and draws the reader's eye.
Why Does This Exist?
Before pseudo-elements, developers had to wrap the first letter in a <span> tag manually:
<!-- OLD way — messy, requires editing HTML -->
<p><span class="dropcap">O</span>nce upon a time...</p>With ::first-letter, no HTML changes are needed:
/* MODERN way — pure CSS, no HTML change needed */
p::first-letter {
font-size: 3em;
color: crimson;
}Simple Example
<!-- HTML -->
<p>Once upon a time, in a land far away, there lived a young web developer
who knew nothing about CSS pseudo-elements. One day, they discovered the
magic of the double colon and their designs were never the same again.</p>/* CSS */
p::first-letter {
font-size: 3em;
color: crimson;
font-weight: bold;
}Expected Output:
___
| |
| O |nce upon a time, in a land far away,
|___|there lived a young web developer...The letter "O" renders at three times the normal font size and in crimson red. All other letters render normally.
Properties Allowed on ::first-letter
Not every CSS property works with ::first-letter. Here are the ones that do:
- Font properties:
font-family,font-size,font-weight,font-style,font-variant,line-height - Colour:
color - Background:
background-color,background-image - Spacing:
margin,padding,border - Layout:
float,vertical-align,text-decoration,text-transform
Important rule:
::first-letteronly works on block-level elements (like<p>,<div>,<h1> · <h6>). It does NOT work on inline elements like<span>or<a>.
Targeting Specific Elements
You can target only certain paragraphs with a class:
<!-- HTML -->
<p class="intro">This paragraph will have a styled first letter.</p>
<p>This paragraph will NOT have a styled first letter.</p>/* CSS */
p.intro::first-letter {
font-size: 2.5em;
color: navy;
font-weight: bold;
float: left; /* Float the letter so text wraps around it */
margin-right: 4px; /* Space between the large letter and the rest */
line-height: 0.85; /* Keeps the large letter vertically aligned */
}Expected Output: Only the first <p> gets the oversized letter. The second paragraph renders normally.
💭 Think about it: What happens if the first character is a punctuation mark like
"or'? The pseudo-element includes it along with the first letter. What if there are spaces before the text? Spaces are skipped · it finds the actual first letter.
::first-line · Style Just the First Line of Text
What Is It?
::first-line targets the first rendered line of text inside a block element. This is dynamic · the "first line" changes depending on the screen width, font size, and browser window size.
Why Does This Exist?
In printed books and quality editorial design, the opening line of a chapter often receives special treatment: a different font, small-caps, or a slightly different colour. ::first-line brings this control to the web.
Simple Example
<!-- HTML -->
<p>The history of the internet is a story of human ingenuity,
collaboration, and the relentless pursuit of connection. What began
as a military research project in the 1960s grew into the most
significant communication network ever built by humankind.</p>/* CSS */
p::first-line {
color: darkblue;
font-variant: small-caps;
font-weight: bold;
}Expected Output (in a 500px-wide container):
THE HISTORY OF THE INTERNET IS A STORY OF ← first line: dark blue, small-caps, bold
human ingenuity, collaboration, and the ← normal styling from here on
relentless pursuit of connection...Only the first line gets the special styling. Every other line renders with the default paragraph styles.
Dynamic First Line
The first line changes as the browser window resizes. If the window narrows, fewer words fit on the first line, so the styled portion shrinks. If the window widens, more words fit, so the styled portion grows. You never need to update your CSS · the browser handles this automatically.
Properties Allowed on ::first-line
Like ::first-letter, only a subset of CSS properties work:
- Font properties:
font-family,font-size,font-weight,font-style,font-variant,line-height - Colour:
color - Background:
background-color - Text:
text-decoration,text-transform,word-spacing,letter-spacing,vertical-align
Important rule:
::first-lineonly works on block-level elements. It does NOT work on inline elements.
Second Example · Combining ::first-line and ::first-letter
You can use both on the same element simultaneously! CSS applies them independently:
/* CSS */
p::first-line {
color: #4a90d9;
font-variant: small-caps;
}
p::first-letter {
font-size: 4em;
color: #e74c3c;
float: left;
margin-right: 5px;
line-height: 0.8;
}Expected Output: The first letter is large and red (from ::first-letter). The rest of the first line is blue and in small-caps (from ::first-line). Lines two and onwards are unstyled.
💭 Think about it: When both
::first-letterand::first-linetarget the same letter, which styles win? Because::first-letteris more specific (it targets a single character inside the first line),::first-letterstyles take precedence over::first-linestyles on that one character.
These are the most powerful and widely used pseudo-elements. They let you inject virtual content into the page · decorations, icons, labels, shapes · all using pure CSS, with zero HTML changes.
Understanding the Mental Model
Think of every HTML element as having three parts:
[::before content] [actual HTML content] [::after content]For example, this HTML:
<p>Hello World</p>With ::before and ::after, it acts as if it were:
<!-- Not real HTML — just how the browser treats it -->
<p>
<pseudo-before>★ </pseudo-before>
Hello World
<pseudo-after> ★</pseudo-after>
</p>The "pseudo-before" and "pseudo-after" elements exist only in the browser's rendering · they are not in the HTML source, not in the DOM (Document Object Model), and cannot be selected with JavaScript like normal elements.
The content Property · The Heart of ::before and ::after
This is critical: ::before and ::after pseudo-elements MUST have a content property to render anything. Without content, they are completely invisible · they do not appear at all.
Even if you want an empty pseudo-element (for purely visual/shape purposes), you must write:
.box::before {
content: ""; /* Empty string — element exists but shows nothing as text */
/* Then you style it with width, height, background, etc. */
}The content Property Values
The content property accepts several types of values:
1. A Text String
Inserts literal text. Enclose the text in quotes.
p::before {
content: "Read this → "; /* Inserts this text before the paragraph */
color: crimson;
font-weight: bold;
}Expected Output:
Read this → Once upon a time, in a land far away...2. An Empty String ""
Inserts nothing visible as text, but the pseudo-element still exists. Used for purely decorative shapes, lines, and overlays.
.card::after {
content: ""; /* No text */
display: block;
width: 100%;
height: 3px;
background-color: #4a90d9; /* Decorative bottom border */
}3. A URL (Image)
Inserts an image from a URL. The image appears inline.
a::before {
content: url("icon-link.png"); /* Adds a link icon before every anchor */
}4. attr() · Pull a Value from an HTML Attribute
The attr() function reads an attribute from the HTML element and uses its value as the content. This is powerful for tooltips, print stylesheets, and data-driven labels.
<!-- HTML -->
<a href="https://example.com">Visit Example</a>/* CSS — adds the URL in brackets after the link text */
a::after {
content: " (" attr(href) ")";
color: gray;
font-size: 0.8em;
}Expected Output:
Visit Example (https://example.com)This technique is extremely useful for print stylesheets · when someone prints a webpage, they can see the actual URLs of links.
5. open-quote and close-quote
Automatically inserts the correct quotation mark characters based on the language and nesting level.
<!-- HTML -->
<p class="quote">The best way to learn is by doing.</p>/* CSS */
p.quote::before {
content: open-quote; /* Inserts " */
font-size: 1.5em;
color: #888;
}
p.quote::after {
content: close-quote; /* Inserts " */
font-size: 1.5em;
color: #888;
}Expected Output:
"The best way to learn is by doing."6. A Counter
Automatically numbers elements using CSS counters.
body {
counter-reset: section; /* Initialise a counter called "section" */
}
h2::before {
counter-increment: section; /* Increase the counter by 1 each time */
content: "Section " counter(section) ": "; /* Output the number */
color: #4a90d9;
font-weight: bold;
}Expected Output (with three <h2> headings):
Section 1: Introduction
Section 2: Core Concepts
Section 3: Practice Exercises::before · Inserting Content Before an Element
What Is It?
::before creates a virtual child element that is placed as the first child of the selected element · before all other content inside it.
Simple Example · Alert Label
<!-- HTML -->
<p class="warning">Please save your work before closing the browser.</p>/* CSS */
.warning::before {
content: "⚠ Warning: ";
color: darkorange;
font-weight: bold;
}Expected Output:
⚠ Warning: Please save your work before closing the browser.The "⚠ Warning: " text is injected by CSS · not present in the HTML at all.
Second Example · Decorative Shape (Empty Content)
<!-- HTML -->
<h2 class="section-title">Our Services</h2>/* CSS */
.section-title {
position: relative;
padding-left: 20px;
}
.section-title::before {
content: ""; /* Empty — no text */
position: absolute;
left: 0;
top: 4px;
width: 8px;
height: 80%;
background-color: #4a90d9; /* A blue vertical bar before the heading */
border-radius: 2px;
}Expected Output: A blue vertical bar on the left side of the heading text · a common design pattern on dashboards and content sites.
::after · Inserting Content After an Element
What Is It?
::after creates a virtual child element that is placed as the last child of the selected element · after all other content inside it.
Simple Example · Required Field Marker
<!-- HTML — a form label -->
<label class="required">Email Address</label>
<input type="email" />/* CSS */
label.required::after {
content: " *";
color: red;
font-weight: bold;
}Expected Output:
Email Address * [input box]The red asterisk is generated by CSS. Every <label> with class required automatically gets the marker without any HTML changes.
Second Example · External Link Indicator
A professional design pattern: add an arrow icon after all external links so users know they are leaving the site.
/* CSS */
a[href^="https://"]::after { /* Targets only links starting with https:// */
content: " ↗";
font-size: 0.8em;
color: #888;
}Expected Output:
Visit our partner site ↗
Read the documentation ↗
Back to Homepage ← (internal link — no arrow)Third Example · Clearfix (Classic Layout Technique)
One of the most famous uses of ::after is the clearfix technique, which fixes a layout problem with floated elements. You will learn about floats in detail in a later lesson, but here is a preview:
/* CSS — the clearfix */
.clearfix::after {
content: ""; /* Must be present */
display: table; /* Creates a block formatting context */
clear: both; /* Clears floated children */
}This pattern is used in millions of websites and frameworks. It works because the empty ::after element causes the parent container to correctly wrap around all its floated children.
::before vs ::after · Key Differences
| Feature | ::before | ::after |
|---|---|---|
| Position in element | First child (before real content) | Last child (after real content) |
| Stacking (z-order) | Behind ::after by default | On top of ::before by default |
content required? | Yes | Yes |
| Can be positioned? | Yes (position: absolute etc.) | Yes |
| Visible in DOM inspector? | Yes (as pseudo-elements) | Yes (as pseudo-elements) |
| Selectable by JavaScript? | Not directly | Not directly |
What Is It?
Every list item (<li>) has an automatically generated marker · the bullet point (•) for unordered lists or the number (1, 2, 3…) for ordered lists. The ::marker pseudo-element lets you style this marker directly.
Before ::marker existed, styling list markers required workarounds: hiding the default marker with list-style: none and creating fake markers with ::before. Now you can style the real marker directly.
Simple Example · Coloured Bullets
<!-- HTML -->
<ul>
<li>HTML — the skeleton</li>
<li>CSS — the skin</li>
<li>JavaScript — the muscles</li>
</ul>/* CSS */
li::marker {
color: #e74c3c; /* Red bullet points */
font-size: 1.3em; /* Slightly larger bullets */
}Expected Output:
● HTML — the skeleton
● CSS — the skin
● JavaScript — the musclesThe bullet colour and size change without touching the list item text itself.
Example · Styled Ordered List Numbers
<!-- HTML -->
<ol>
<li>Plan your project</li>
<li>Write the HTML structure</li>
<li>Apply CSS styles</li>
<li>Test in the browser</li>
</ol>/* CSS */
ol li::marker {
color: #4a90d9;
font-weight: bold;
font-size: 1.1em;
}Expected Output:
1. Plan your project
2. Write the HTML structure
3. Apply CSS styles
4. Test in the browserThe numbers are blue and bold. The list item text remains in the default colour.
Changing the Marker Symbol with content
You can also use the content property on ::marker to completely replace the default bullet or number:
/* CSS */
li::marker {
content: "✅ "; /* Replace bullet with a checkmark */
}Expected Output:
✅ Plan your project
✅ Write the HTML structure
✅ Apply CSS stylesCSS Properties Allowed on ::marker
Only a limited set of properties work on ::marker:
colorfontproperties (font-family,font-size,font-weight,font-style)contentunicode-bidi,direction- Animation and transition properties
You cannot use margin, padding, background, or most layout properties on ::marker.
::marker · Styling List Bullets and Numbers. Open your editor, type the examples above by hand, modify them, and observe what changes.What Is It?
When a user clicks and drags to highlight text on a webpage, the browser applies a default blue highlight colour. The ::selection pseudo-element lets you customise that highlight to match your brand's colour scheme.
Why Use It?
Brand consistency. Many professional websites customise the selection colour so that even the act of highlighting text feels on-brand. It is a small but memorable detail.
Simple Example
/* CSS */
::selection {
background-color: #ffcc00; /* Yellow highlight */
color: #000000; /* Black text */
}Expected Output: When a user selects any text on the page, it highlights in yellow with black text instead of the default browser blue.
Targeting Specific Elements
You can apply ::selection to specific elements only:
/* CSS */
p::selection {
background-color: #4a90d9; /* Blue highlight for paragraphs */
color: white;
}
h1::selection {
background-color: #e74c3c; /* Red highlight for headings */
color: white;
}CSS Properties Allowed on ::selection
Very few properties work on ::selection:
colorbackground-color(notbackgroundshorthand)text-decorationtext-shadowcursoroutline
Note: You cannot change
font-size,padding,margin, or most other properties via::selection.
::selection · Styling Highlighted Text. Open your editor, type the examples above by hand, modify them, and observe what changes.Pseudo-elements can be combined with class selectors, element selectors, pseudo-classes, and attribute selectors. This is where they become extremely powerful.
With a Class Selector
/* Only paragraphs with class="intro" get the styled first letter */
p.intro::first-letter {
font-size: 2.5em;
color: #e74c3c;
float: left;
margin-right: 4px;
}With a Pseudo-class (Hover Effect)
You can combine :hover with ::after to create hover-triggered content:
/* A tooltip that appears on hover */
.tooltip-item {
position: relative;
cursor: help;
}
.tooltip-item::after {
content: attr(data-tip); /* Read from the data-tip HTML attribute */
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0; /* Hidden by default */
pointer-events: none;
}
.tooltip-item:hover::after {
opacity: 1; /* Visible on hover */
}<!-- HTML -->
<span class="tooltip-item" data-tip="This is a tooltip!">Hover over me</span>Expected Output: The text "Hover over me" appears normally. When hovered, a dark tooltip box appears above it saying "This is a tooltip!"
With an Attribute Selector
/* Add a PDF icon after links pointing to PDF files */
a[href$=".pdf"]::after {
content: " 📄"; /* Only links ending in .pdf get the icon */
font-size: 0.8em;
}<!-- HTML -->
<a href="report.pdf">Download Report</a> ← Gets the 📄 icon
<a href="homepage.html">Go Home</a> ← No iconExpected Output:
Download Report 📄
Go HomeExercise 1 · Drop Cap Paragraph
Objective: Apply a classic drop cap effect to the opening paragraph of an article.
Scenario: You are designing a digital magazine. The editor wants the opening paragraph of every article to begin with a large, bold drop cap letter in the magazine's brand colour (#8B1A1A · a deep red).
Steps:
- Create an HTML page with a
<div class="article">container. - Inside it, add a
<p class="opening">and fill it with two or three sentences. - Add a second
<p>after it with regular text. - Apply
::first-letteronly to.opening, not to the second paragraph.
Your Code: