What Is It?
What Are Pseudo-classes and Pseudo-elements?
Pseudo-classes and pseudo-elements are two of the most powerful features in CSS. They let you style elements based on state, position, or relationship, and even insert content that does not exist in the HTML.
Pseudo-classes (single colon)
A pseudo-class selects an element based on a state or position. Examples: :hover (when the user points at it), :focus (when it is keyboard-focused), :nth-child(2n) (when it is the even-numbered child), :checked (when a checkbox is ticked).
button:hover { background: purple; }
input:focus { outline: 2px solid cyan; }
li:nth-child(odd) { background: #f1f5f9; }Pseudo-elements (double colon)
A pseudo-element targets a part of an element, or injects a 'fake' element. Examples: ::before and ::after inject content, ::first-letter targets the first character, ::selection styles selected text, ::placeholder styles the placeholder inside inputs.
.card::before {
content: 'NEW';
background: red;
color: white;
padding: 4px 8px;
}Why two colons for pseudo-elements? In CSS3, pseudo-elements got their own double-colon syntax (::before) to distinguish them from pseudo-classes. Browsers still accept the old single-colon form for backwards compatibility, but double colons are the modern standard.
Why Does It Matter?
Why Learn Pseudo-classes and Pseudo-elements?
1. Interactivity Without JavaScript
Hover effects, focus rings, checked states, form validation styling, all handled with CSS. No JavaScript, no event listeners, no bundle size. :hover, :focus, :valid, and :invalid handle the majority of interactive UI in a browser.
2. Less HTML, More Creativity
Need a decorative corner on a card? A tooltip arrow? A quote mark before a blockquote? A ribbon banner? None of these need extra HTML elements. ::before and ::after let you inject the content purely from CSS, keeping your HTML clean and semantic.
3. Pattern-Based Styling
Style every other row of a table, every third item in a grid, or only the last element of a list, without adding class names. :nth-child() is like a tiny query language for DOM position. Anika styled her entire homework table in 3 lines of CSS.
4. Modern Selectors Unlock New Patterns
Newer pseudo-classes like :is(), :where(), and especially :has() (the parent selector) let you write cleaner CSS and style elements based on what they contain. Things that used to require JavaScript now work with a single selector.
5. Accessibility for Free
:focus-visible shows focus rings only for keyboard users, not mouse clicks. :hover does nothing on touch devices, so you never punish mobile users. Semantic pseudo-classes make your site work well for every input device.
Detailed Explanation
A Tour of the Most Useful Pseudo-classes and Pseudo-elements
1. Interaction States
a:link { color: #7c3aed; } /* unvisited */
a:visited { color: #db2777; } /* already visited */
a:hover { color: #ec4899; text-decoration: underline; }
a:active { color: #f43f5e; } /* the instant it is clicked */
button:focus { outline: 2px solid #06b6d4; }
button:focus-visible { outline: 2px solid #06b6d4; } /* keyboard only */
button:disabled { opacity: 0.5; cursor: not-allowed; }:focus-visible is the modern choice: it only shows the focus ring when the user navigates with the keyboard, not when they click with a mouse. Clean and accessible.
2. Form States
input:required { border-left: 3px solid #f59e0b; }
input:valid { border-color: #10b981; }
input:invalid { border-color: #ef4444; }
input:disabled { background: #f1f5f9; }
input:checked { accent-color: #a855f7; }
/* Placeholder-shown: input is empty */
input:placeholder-shown { font-style: italic; color: #94a3b8; }This lets you show live validation feedback without any JavaScript. A red border appears the moment the user's input becomes invalid.
3. Structural / Positional
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(2) { color: red; }
li:nth-child(odd) { background: #f8fafc; } /* 1st, 3rd, 5th... */
li:nth-child(even) { background: #ffffff; } /* 2nd, 4th, 6th... */
li:nth-child(3n) { color: purple; } /* every 3rd */
li:nth-child(3n+1) { color: teal; } /* 1st, 4th, 7th... */
li:only-child { color: gray; } /* the only child of its parent */
li:empty { display: none; } /* has no content */Zebra stripes for tables take 2 lines. Highlighting every third grid item takes 1. This is what designers mean when they say 'pattern-based' styling.
4. Negation and Grouping: :not(), :is(), :where()
/* Not: exclude something */
li:not(.active) { color: gray; }
button:not(:disabled):hover { background: purple; }
/* Is: group selectors without copying */
:is(h1, h2, h3) { font-family: 'Syne', sans-serif; }
/* Equivalent to: h1, h2, h3 { ... } but composable */
/* Where: like :is() but with zero specificity */
:where(article, section) p { line-height: 1.7; }
/* zero specificity means it is easy to override */5. The Parent Selector: :has()
For years CSS only looked downward. :has() finally lets you select a parent based on its children. This is the biggest new CSS feature in a decade.
/* Style a card that contains an image */
.card:has(img) { padding: 0; }
/* Style a form that has an invalid input */
form:has(:invalid) button { opacity: 0.5; pointer-events: none; }
/* Style a label that has a checked checkbox */
label:has(input:checked) { background: #7c3aed; color: white; }6. Target and Empty
section:target { background: #fef3c7; } /* the section matching #hash in URL */
div:empty { display: none; } /* element with no content at all */7. Pseudo-elements: ::before and ::after
These inject a 'fake' element at the start or end of an element's content. They need the content property to render.
blockquote::before { content: '"'; font-size: 64px; color: #a855f7; }
blockquote::after { content: '"'; font-size: 64px; color: #a855f7; }
.new::after {
content: 'NEW';
background: red;
color: white;
padding: 2px 8px;
border-radius: 999px;
margin-left: 8px;
font-size: 11px;
}
.tooltip::after {
content: attr(data-tip); /* read the data-tip attribute */
position: absolute;
bottom: -30px; left: 0;
background: black; color: white;
padding: 4px 8px; border-radius: 6px;
opacity: 0; transition: opacity 0.2s;
}
.tooltip:hover::after { opacity: 1; }8. Text Pseudo-elements
p::first-letter { font-size: 3em; float: left; color: #7c3aed; }
p::first-line { font-weight: bold; }
::selection { background: #a855f7; color: white; }
::placeholder { color: #94a3b8; font-style: italic; }
li::marker { color: #7c3aed; font-size: 1.2em; }::selection styles the highlighted text when a user selects anything. ::placeholder styles the hint text inside form inputs. ::marker targets the bullet or number of a list item. You can finally color bullets without extra markup.
9. Creative Patterns
/* Ribbon on a card */
.card::before {
content: 'SALE';
position: absolute; top: 12px; right: -28px;
background: #ef4444; color: white;
padding: 4px 36px; font-size: 12px;
transform: rotate(45deg);
}
/* Animated underline on hover */
.nav a { position: relative; }
.nav a::after {
content: '';
position: absolute;
left: 0; bottom: -4px;
width: 100%; height: 2px;
background: #a855f7;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s;
}
.nav a:hover::after {
transform: scaleX(1);
transform-origin: left;
}No extra HTML, no JavaScript, just a sliding underline that knows which direction to grow from.
Code Examples
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f8fafc; padding: 40px; }
h1 { color: #7c3aed; margin-bottom: 16px; }
table { border-collapse: collapse; width: 100%; max-width: 700px; background: white; box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 12px; overflow: hidden; }
th { background: #7c3aed; color: white; padding: 14px; text-align: left; }
td { padding: 12px 14px; color: #334155; }
tr:nth-child(even) { background: #f1f5f9; }
tr:hover { background: #ede9fe; }
tr:last-child td { font-weight: bold; color: #7c3aed; }
</style>
</head>
<body>
<h1>Student Scores</h1>
<table>
<thead><tr><th>Student</th><th>Chapter</th><th>Score</th></tr></thead>
<tbody>
<tr><td>Aarav</td><td>Variables</td><td>94</td></tr>
<tr><td>Priya</td><td>Variables</td><td>91</td></tr>
<tr><td>Rohan</td><td>Variables</td><td>88</td></tr>
<tr><td>Ishita</td><td>Variables</td><td>97</td></tr>
<tr><td>Vivaan</td><td>Variables</td><td>85</td></tr>
<tr><td>Anika</td><td>Variables</td><td>99</td></tr>
<tr><td>Class Average</td><td>---</td><td>92.3</td></tr>
</tbody>
</table>
</body>
</html>tr:nth-child(even) gives every even-numbered row a light background for zebra striping. tr:hover highlights the row the user is pointing at. tr:last-child td styles the summary row. All three effects come from pseudo-classes with zero extra HTML.<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: #f1f5f9; padding: 80px 40px; display: flex; gap: 40px; justify-content: center; }
.tip {
position: relative;
padding: 12px 24px;
background: #7c3aed;
color: white;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
}
.tip::after {
content: attr(data-tip);
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
transform: translateX(-50%);
background: #0b1120;
color: #e2e8f0;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 400;
white-space: nowrap;
box-shadow: 0 6px 16px rgba(0,0,0,0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
}
.tip::before {
content: '';
position: absolute;
left: 50%;
bottom: calc(100% + 4px);
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #0b1120;
opacity: 0;
transition: opacity 0.2s;
}
.tip:hover::after,
.tip:hover::before {
opacity: 1;
transform: translateX(-50%) translateY(-4px);
}
</style>
</head>
<body>
<button class="tip" data-tip="Meera's latest project">Hover me</button>
<button class="tip" data-tip="Click to save changes">Save</button>
<button class="tip" data-tip="Warning: this deletes everything">Delete</button>
</body>
</html>data-tip attribute. The CSS injects that attribute's text via ::after using content: attr(data-tip). The ::before draws a tiny triangle pointer. Both are hidden with opacity: 0 and revealed on hover. No JavaScript, one CSS rule per button.<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Georgia', serif; background: #fafaf9; padding: 60px 40px; color: #0f172a; max-width: 720px; margin: 0 auto; }
blockquote {
position: relative;
background: white;
padding: 48px 48px 48px 64px;
border-left: 5px solid #7c3aed;
border-radius: 0 12px 12px 0;
font-size: 22px;
line-height: 1.5;
color: #334155;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
margin: 40px 0;
}
blockquote::before {
content: open-quote;
position: absolute;
top: -20px;
left: 12px;
font-size: 120px;
line-height: 1;
color: #a855f7;
font-family: 'Georgia', serif;
}
blockquote::after {
content: '';
position: absolute;
right: 20px; bottom: 20px;
width: 40px; height: 3px;
background: linear-gradient(90deg, #a855f7, #ec4899);
border-radius: 2px;
}
blockquote cite {
display: block;
margin-top: 16px;
font-size: 14px;
color: #7c3aed;
font-style: normal;
font-weight: 700;
}
</style>
</head>
<body>
<blockquote>
Learning to code changed how I think about problems. CSS variables and pseudo-elements made me fall in love with design.
<cite>- Ishita, age 15</cite>
</blockquote>
<blockquote>
You do not need a JavaScript library for nice quotation marks. ::before and ::after handle it beautifully.
<cite>- Meera, age 13</cite>
</blockquote>
</body>
</html>::before uses content: open-quote to render a large purple quote mark floating above the top-left. ::after draws a small decorative gradient bar in the bottom-right. No extra HTML, and the quotes scale with the text automatically.<!DOCTYPE html>
<html>
<head>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: white; min-height: 100vh; }
nav { display: flex; gap: 32px; padding: 24px 40px; background: #0b1120; border-bottom: 1px solid #1e293b; }
nav a {
position: relative;
color: #cbd5e1;
text-decoration: none;
font-size: 16px;
padding: 6px 0;
transition: color 0.2s;
}
nav a::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #a855f7, #06b6d4);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
nav a:hover {
color: white;
}
nav a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
main { padding: 80px 40px; text-align: center; }
h1 { color: #a855f7; font-size: 40px; }
</style>
</head>
<body>
<nav>
<a href="#">Home</a>
<a href="#">Projects</a>
<a href="#">Blog</a>
<a href="#">Contact</a>
</nav>
<main>
<h1>Vivaan's Portfolio</h1>
</main>
</body>
</html>::after. It starts at scaleX(0) (invisible) with transform-origin: right. On hover it scales to 1 with transform-origin: left, creating a slide-in effect. When the mouse leaves, it retracts back to the right. Pure CSS, no JavaScript.<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f8fafc; padding: 40px; display: flex; justify-content: center; }
.form { background: white; padding: 32px; border-radius: 16px; max-width: 400px; width: 100%; box-shadow: 0 10px 30px rgba(0,0,0,0.08); }
h2 { color: #0f172a; margin-bottom: 24px; }
label { display: block; margin-bottom: 16px; font-size: 13px; color: #475569; }
input {
display: block;
width: 100%;
padding: 12px;
margin-top: 6px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
input:focus { border-color: #7c3aed; }
input:placeholder-shown { font-style: italic; }
input:not(:placeholder-shown):valid { border-color: #10b981; }
input:not(:placeholder-shown):invalid { border-color: #ef4444; }
input:required + .req::before { content: ' *'; color: #ef4444; }
button {
width: 100%;
padding: 12px;
background: #7c3aed;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
}
button:disabled { opacity: 0.4; cursor: not-allowed; }
form:has(input:invalid) button { opacity: 0.4; cursor: not-allowed; }
</style>
</head>
<body>
<form class="form">
<h2>Sign Up</h2>
<label>Name<input type="text" placeholder="Your name" minlength="2" required></label>
<label>Email<input type="email" placeholder="you@example.com" required></label>
<label>Age<input type="number" placeholder="10-18" min="10" max="18" required></label>
<button type="submit">Create Account</button>
</form>
</body>
</html>:not(:placeholder-shown)). The form:has(input:invalid) button rule disables the submit button whenever any input is invalid. All validation styling is pure CSS, no JavaScript.<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #f8fafc; padding: 40px; display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); max-width: 1000px; margin: 0 auto; }
.product {
position: relative;
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
overflow: hidden;
}
.product h3 { color: #0f172a; font-size: 20px; margin-bottom: 8px; }
.product p { color: #64748b; font-size: 14px; line-height: 1.6; }
.product .price { display: block; margin-top: 16px; font-size: 24px; font-weight: 700; color: #7c3aed; }
/* Ribbon on the top-right corner */
.product.sale::before {
content: 'SALE';
position: absolute;
top: 18px; right: -40px;
background: #ef4444;
color: white;
padding: 6px 46px;
font-size: 12px;
font-weight: 800;
letter-spacing: 1px;
transform: rotate(45deg);
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
/* NEW badge */
.product.new::after {
content: 'NEW';
position: absolute;
top: 16px; left: 16px;
background: linear-gradient(135deg, #a855f7, #ec4899);
color: white;
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
}
</style>
</head>
<body>
<div class="product sale">
<h3>Ishita's HTML Book</h3>
<p>Learn HTML and CSS from scratch with 50 projects.</p>
<span class="price">Rs 499</span>
</div>
<div class="product new">
<h3>Aarav's CSS Guide</h3>
<p>Master CSS variables, Grid, and animations.</p>
<span class="price">Rs 399</span>
</div>
<div class="product sale new">
<h3>Kavya's JS Bundle</h3>
<p>JavaScript basics to advanced in one bundle.</p>
<span class="price">Rs 899</span>
</div>
</body>
</html>.product.sale::before injects a rotated red ribbon in the corner. .product.new::after injects a pill-shaped gradient badge in the top-left. The third card uses both classes and shows both decorations at once. No extra markup, just pseudo-elements.Common Mistakes
Forgetting the content Property on ::before and ::after
.card::before {
background: red;
width: 20px;
height: 20px;
/* No content property - nothing renders! */
}.card::before {
content: ''; /* Even an empty string works */
background: red;
width: 20px;
height: 20px;
display: block;
}content property is required for ::before and ::after. An empty string content: '' is fine when you only want a decorative shape. Without content, the browser simply does not render the pseudo-element.Misreading :nth-child() Formulas
/* Wanted every 3rd row, starting from the first */
tr:nth-child(3n) {
background: #f1f5f9;
}
/* This selects the 3rd, 6th, 9th... - NOT the 1st, 4th, 7th! */tr:nth-child(3n+1) {
background: #f1f5f9;
/* Now selects 1st, 4th, 7th... */
}an+b where n goes 0, 1, 2, 3... and the result must be a positive integer. 3n+1 gives 1, 4, 7, 10. 3n gives 3, 6, 9. Sketch a few values to check your formula.Single Colon vs Double Colon Confusion
.card:before {
content: 'NEW';
}
/* This works but is the old syntax */.card::before {
content: 'NEW';
}
/* Modern CSS3 syntax - use this */:hover, :first-child). Pseudo-elements use two colons (::before, ::after, ::first-letter). The double colon was introduced in CSS3 to distinguish them. Always use two colons for pseudo-elements in new code.Using :hover on Interactive States Alone (No Keyboard Support)
button:hover {
background: purple;
outline: 2px solid cyan;
}
/* Keyboard users see no feedback! */button:hover,
button:focus-visible {
background: purple;
outline: 2px solid cyan;
}:hover with :focus-visible (or :focus) so keyboard users see the same affordance as mouse users. :focus-visible is preferred because it only shows the ring for keyboard focus, not for mouse clicks.Summary
- Pseudo-classes (single colon, like <code>:hover</code>, <code>:focus</code>, <code>:nth-child</code>) select elements based on state or position. Pseudo-elements (double colon, like <code>::before</code>, <code>::after</code>, <code>::first-letter</code>) target parts of an element or inject new content.
- <code>:hover</code>, <code>:focus</code>, <code>:focus-visible</code>, <code>:active</code>, <code>:disabled</code>, and <code>:checked</code> handle the majority of interactive UI states without JavaScript. Always pair <code>:hover</code> with <code>:focus-visible</code> for accessibility.
- Form validation styling is free: <code>:valid</code>, <code>:invalid</code>, <code>:required</code>, and <code>:placeholder-shown</code> let you show live feedback as the user types. Combine <code>:not(:placeholder-shown)</code> with <code>:invalid</code> to only show red after the user has started typing.
- <code>:nth-child(an+b)</code> is a tiny query language: <code>odd</code>, <code>even</code>, <code>2n</code>, <code>3n+1</code>, etc. Use it for zebra stripes, grid patterns, and highlighting specific items without extra classes.
- <code>:not()</code> excludes, <code>:is()</code> groups selectors without rewriting them, and <code>:where()</code> is like <code>:is()</code> but with zero specificity (useful for base styles that are easy to override).
- <code>:has()</code> is the parent selector, finally. Style an element based on what it contains: <code>form:has(:invalid) button</code>, <code>.card:has(img)</code>, <code>label:has(input:checked)</code>. This used to require JavaScript.
- <code>::before</code> and <code>::after</code> require a <code>content</code> property to render. Even <code>content: ''</code> is enough for a pure decoration. They can read attributes with <code>content: attr(data-tip)</code>, perfect for tooltips.
- <code>::first-letter</code> and <code>::first-line</code> style the first character or line of a paragraph (drop caps, magazine styling). <code>::selection</code> styles highlighted text. <code>::placeholder</code> styles form placeholders. <code>::marker</code> styles list bullets or numbers.
- Creative patterns are mostly pseudo-elements: ribbons, badges, tooltips, sliding underlines, quote marks, custom bullets, decorative lines. All without adding a single extra HTML element.