Techbase Academy
CSS · Lesson 30

CSS Pseudo-elements · Styling Parts of Elements Without Changing Your HTML

10 phasesProject OverviewGitHub Pages
Phase 1 of 10
Lesson Introduction

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 content property 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

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 10
Prerequisite Concepts

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.

css
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:

css
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 :.

css
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 ::.

css
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)

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 10
Part 1 · What Is a Pseudo-element?

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:

css
selector::pseudo-element {
 css-property: value;
}

Examples:

css
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-elementWhat It TargetsCategory
::first-letterThe very first letter of a block of textText
::first-lineThe first visible line of a block of textText
::beforeA virtual element inserted before the contentContent
::afterA virtual element inserted after the contentContent
::markerThe bullet or number of a list itemContent
::selectionThe text highlighted/selected by the userContent

✏️ Your Task
Practise what you just learned about What Is a Pseudo-element?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 10
Part 2 · Text Pseudo-elements

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:

html
<!-- 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:

css
/* MODERN way — pure CSS, no HTML change needed */
p::first-letter {
 font-size: 3em;
 color: crimson;
}

Simple Example

html
<!-- 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
/* CSS */
p::first-letter {
 font-size: 3em;
 color: crimson;
 font-weight: bold;
}

Expected Output:

code
 ___
|   |
| 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-letter only 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
<!-- 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
/* 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
<!-- 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
/* CSS */
p::first-line {
 color: darkblue;
 font-variant: small-caps;
 font-weight: bold;
}

Expected Output (in a 500px-wide container):

code
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-line only 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
/* 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-letter and ::first-line target the same letter, which styles win? Because ::first-letter is more specific (it targets a single character inside the first line), ::first-letter styles take precedence over ::first-line styles on that one character.


✏️ Your Task
Practise what you just learned about Text Pseudo-elements. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 10
Part 3 · Content Pseudo-elements: `::before` and `::after`

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:

code
[::before content] [actual HTML content] [::after content]

For example, this HTML:

html
<p>Hello World</p>

With ::before and ::after, it acts as if it were:

html
<!-- 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:

css
.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.

css
p::before {
 content: "Read this → "; /* Inserts this text before the paragraph */
 color: crimson;
 font-weight: bold;
}

Expected Output:

code
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.

css
.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.

css
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
<!-- HTML -->
<a href="https://example.com">Visit Example</a>
css
/* CSS — adds the URL in brackets after the link text */
a::after {
 content: " (" attr(href) ")";
 color: gray;
 font-size: 0.8em;
}

Expected Output:

code
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
<!-- HTML -->
<p class="quote">The best way to learn is by doing.</p>
css
/* 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:

code
"The best way to learn is by doing."

6. A Counter

Automatically numbers elements using CSS counters.

css
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):

code
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
<!-- HTML -->
<p class="warning">Please save your work before closing the browser.</p>
css
/* CSS */
.warning::before {
 content: "⚠ Warning: ";
 color: darkorange;
 font-weight: bold;
}

Expected Output:

code
⚠ 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
<!-- HTML -->
<h2 class="section-title">Our Services</h2>
css
/* 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
<!-- HTML — a form label -->
<label class="required">Email Address</label>
<input type="email" />
css
/* CSS */
label.required::after {
 content: " *";
 color: red;
 font-weight: bold;
}

Expected Output:

code
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
/* CSS */
a[href^="https://"]::after { /* Targets only links starting with https:// */
 content: " ↗";
 font-size: 0.8em;
 color: #888;
}

Expected Output:

code
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
/* 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 elementFirst child (before real content)Last child (after real content)
Stacking (z-order)Behind ::after by defaultOn top of ::before by default
content required?YesYes
Can be positioned?Yes (position: absolute etc.)Yes
Visible in DOM inspector?Yes (as pseudo-elements)Yes (as pseudo-elements)
Selectable by JavaScript?Not directlyNot directly

✏️ Your Task
` · ### ::before · Inserting Content Before an Element
Phase 6 of 10
Part 4 · `::marker` · Styling List Bullets and Numbers

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
<!-- HTML -->
<ul>
<li>HTML — the skeleton</li>
<li>CSS — the skin</li>
<li>JavaScript — the muscles</li>
</ul>
css
/* CSS */
li::marker {
 color: #e74c3c; /* Red bullet points */
 font-size: 1.3em; /* Slightly larger bullets */
}

Expected Output:

code
● HTML — the skeleton
● CSS — the skin
● JavaScript — the muscles

The bullet colour and size change without touching the list item text itself.

Example · Styled Ordered List Numbers

html
<!-- 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
/* CSS */
ol li::marker {
 color: #4a90d9;
 font-weight: bold;
 font-size: 1.1em;
}

Expected Output:

code
1. Plan your project
2. Write the HTML structure
3. Apply CSS styles
4. Test in the browser

The 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
/* CSS */
li::marker {
 content: "✅ "; /* Replace bullet with a checkmark */
}

Expected Output:

code
✅  Plan your project
✅  Write the HTML structure
✅  Apply CSS styles

CSS Properties Allowed on ::marker

Only a limited set of properties work on ::marker:

  • color
  • font properties (font-family, font-size, font-weight, font-style)
  • content
  • unicode-bidi, direction
  • Animation and transition properties

You cannot use margin, padding, background, or most layout properties on ::marker.


✏️ Your Task
Practise what you just learned about ::marker · Styling List Bullets and Numbers. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 10
Part 5 · `::selection` · Styling Highlighted Text

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
/* 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
/* 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:

  • color
  • background-color (not background shorthand)
  • text-decoration
  • text-shadow
  • cursor
  • outline

Note: You cannot change font-size, padding, margin, or most other properties via ::selection.


✏️ Your Task
Practise what you just learned about ::selection · Styling Highlighted Text. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 8 of 10
Part 6 · Combining Pseudo-elements with Other Selectors

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

css
/* 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:

css
/* 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
<!-- 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

css
/* 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
<!-- HTML -->
<a href="report.pdf">Download Report</a>   ← Gets the 📄 icon
<a href="homepage.html">Go Home</a>        ← No icon

Expected Output:

code
Download Report 📄
Go Home

✏️ Your Task
Practise what you just learned about Combining Pseudo-elements with Other Selectors. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 9 of 10
Part 7 · Guided Practice Exercises
🎯 Your Challenge

Exercise 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:

  1. Create an HTML page with a <div class="article"> container.
  2. Inside it, add a <p class="opening"> and fill it with two or three sentences.
  3. Add a second <p> after it with regular text.
  4. Apply ::first-letter only to .opening, not to the second paragraph.

Your Code:

✏️ Task
All 73 lessons in CSS Styling·Next course: JavaScript ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close