CSS3 & Responsive Design

Pseudo-Classes: Structural & Interactive

45 min Lesson 5 of 60

What Are Pseudo-Classes?

A pseudo-class is a keyword added to a selector that specifies a special state or position of the selected element. Unlike regular classes that you add manually to HTML elements, pseudo-classes are dynamic -- they are automatically applied by the browser based on user interaction, document structure, or element state. Pseudo-classes always begin with a single colon (:) followed by the pseudo-class name.

The general syntax looks like this:

Pseudo-Class Syntax

selector:pseudo-class {
    property: value;
}

/* Examples */
a:hover { color: red; }
li:first-child { font-weight: bold; }
input:focus { border-color: blue; }

Pseudo-classes are incredibly powerful because they let you style elements based on conditions that cannot be expressed with simple selectors. They fall into several categories: interactive (user-driven states), structural (position in the DOM tree), form-related (input states), and miscellaneous (language, target, negation). In this lesson, we will cover all of them in depth.

Note: Do not confuse pseudo-classes (single colon :) with pseudo-elements (double colon ::). Pseudo-classes select elements in a particular state, while pseudo-elements create virtual sub-parts of an element. We will cover pseudo-elements in the next lesson.

Interactive Pseudo-Classes

Interactive pseudo-classes respond to user actions such as hovering, clicking, focusing, and visiting links. These are among the most commonly used pseudo-classes in everyday CSS development.

The :hover Pseudo-Class

The :hover pseudo-class applies when the user places their cursor over an element without clicking it. It is one of the most frequently used pseudo-classes for creating visual feedback on interactive elements like buttons, links, and cards.

Example: Hover Effects on Buttons and Cards

/* Basic button hover */
.btn {
    background-color: #3498db;
    color: white;
    padding: 12px 24px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s ease;
}

.btn:hover {
    background-color: #2980b9;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

/* Card hover effect */
.card {
    padding: 20px;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    transition: box-shadow 0.3s ease;
}

.card:hover {
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}

/* Table row hover */
tr:hover {
    background-color: #f5f5f5;
}

/* Navigation link underline on hover */
nav a {
    text-decoration: none;
    position: relative;
}

nav a:hover::after {
    content: '';
    position: absolute;
    bottom: -2px;
    left: 0;
    width: 100%;
    height: 2px;
    background-color: currentColor;
}
Tip: Always pair :hover with transition for smooth animations. A transition duration of 0.2s to 0.3s feels natural to users. Without transitions, hover effects appear abrupt and jarring.

The :focus Pseudo-Class

The :focus pseudo-class applies when an element receives focus -- typically when a user clicks on it or navigates to it using the Tab key. This is critical for accessibility because keyboard users rely on visible focus indicators to know which element they are interacting with.

Example: Focus Styles for Inputs and Links

/* Custom focus style for input fields */
input:focus,
textarea:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
}

/* Focus style for buttons */
.btn:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.5);
}

/* Focus style for links */
a:focus {
    outline: 2px solid #3498db;
    outline-offset: 2px;
    border-radius: 2px;
}
Warning: Never use outline: none without providing an alternative visible focus indicator. Removing the default outline without a replacement makes your site inaccessible to keyboard users. If you remove the outline, always replace it with a box-shadow, border, or other visible styling.

The :active Pseudo-Class

The :active pseudo-class applies during the moment an element is being activated -- that is, while the user is pressing down on it (mouse button held down or finger touching the screen). It gives tactile feedback that something is happening.

Example: Active States for Buttons

/* Button press effect */
.btn:active {
    transform: translateY(1px);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}

/* Link active color */
a:active {
    color: #e74c3c;
}

/* Card press-down effect */
.card:active {
    transform: scale(0.98);
}

The :visited Pseudo-Class

The :visited pseudo-class targets links (<a> elements) that the user has already visited. This helps users distinguish between links they have clicked and those they have not.

Example: Visited Link Styling

/* Default link color */
a:link {
    color: #3498db;
}

/* Visited link color */
a:visited {
    color: #8e44ad;
}

/* Search results styling */
.search-result a:visited {
    color: #681da8;
}

.search-result a:visited .result-title {
    color: #681da8;
}
Note: For privacy reasons, browsers severely limit which CSS properties can be applied with :visited. You can only change color, background-color, border-color, outline-color, and the color parts of column-rule-color, text-decoration-color, and fill/stroke for SVG. Properties like font-size, padding, or display are ignored.

The LVHA Order for Links

When styling links, the order in which you declare pseudo-classes matters because of CSS specificity and the cascade. The recommended order is known as LVHA (often remembered with the mnemonic "LoVe HAte"):

  1. :link -- Unvisited links
  2. :visited -- Previously visited links
  3. :hover -- When the cursor is over the link
  4. :active -- While the link is being clicked

Example: Correct LVHA Order

/* L - Link (unvisited) */
a:link {
    color: #3498db;
    text-decoration: none;
}

/* V - Visited */
a:visited {
    color: #8e44ad;
}

/* H - Hover */
a:hover {
    color: #e74c3c;
    text-decoration: underline;
}

/* A - Active */
a:active {
    color: #c0392b;
}
Warning: If you put :hover before :visited, the visited color will override the hover effect because :visited and :hover have the same specificity, and the one declared later wins. Always follow LVHA order to ensure all states work correctly.

The :focus-within Pseudo-Class

The :focus-within pseudo-class applies to an element when any of its descendants receive focus. This is extremely useful for highlighting entire form groups or containers when a user focuses on an input inside them.

Example: Highlighting a Form Group on Focus

/* Highlight the entire form group when any input is focused */
.form-group {
    padding: 16px;
    border: 2px solid transparent;
    border-radius: 8px;
    transition: border-color 0.3s ease, background-color 0.3s ease;
}

.form-group:focus-within {
    border-color: #3498db;
    background-color: #ebf5fb;
}

/* Show a search suggestions dropdown when the search box is focused */
.search-container .suggestions {
    display: none;
}

.search-container:focus-within .suggestions {
    display: block;
}

/* Highlight a table row when an inline input is focused */
tr:focus-within {
    background-color: #fff3cd;
}

The :focus-visible Pseudo-Class

The :focus-visible pseudo-class applies only when the browser determines that focus should be visually indicated. Typically, this means it activates for keyboard navigation (Tab key) but not for mouse clicks. This lets you provide focus indicators for keyboard users while keeping a clean look for mouse users.

Example: Keyboard-Only Focus Indicators

/* Remove default outline for all focus */
button:focus {
    outline: none;
}

/* Add outline only for keyboard navigation */
button:focus-visible {
    outline: 3px solid #3498db;
    outline-offset: 2px;
}

/* Same pattern for links */
a:focus {
    outline: none;
}

a:focus-visible {
    outline: 2px dashed #e74c3c;
    outline-offset: 3px;
}
Tip: Using :focus-visible instead of :focus is now considered a best practice for buttons and links. It provides the best of both worlds: clean design for mouse users and clear navigation indicators for keyboard users.

Structural Pseudo-Classes

Structural pseudo-classes select elements based on their position within the DOM tree -- their relationship to their parent and sibling elements. These are powerful for styling lists, tables, grids, and any repetitive structures without adding extra classes to your HTML.

:first-child and :last-child

The :first-child pseudo-class selects an element that is the first child of its parent. The :last-child pseudo-class selects the last child. These are useful for removing or adding borders, margins, or special styling to the first or last items in a list or container.

Example: First and Last Child Styling

/* Remove top border from the first list item */
.menu-list li:first-child {
    border-top: none;
}

/* Remove bottom border from the last list item */
.menu-list li:last-child {
    border-bottom: none;
}

/* Special styling for the first paragraph in an article */
article p:first-child {
    font-size: 1.2em;
    font-weight: 500;
    color: #2c3e50;
}

/* Remove bottom margin from the last element in a card */
.card > *:last-child {
    margin-bottom: 0;
}

:nth-child(n) -- The Power Selector

The :nth-child(n) pseudo-class is one of the most versatile selectors in CSS. It selects elements based on their position among siblings using a formula. The argument n can be a number, a keyword, or a formula in the form an+b.

  • :nth-child(3) -- Selects the 3rd child exactly.
  • :nth-child(odd) -- Selects the 1st, 3rd, 5th, etc. Same as :nth-child(2n+1).
  • :nth-child(even) -- Selects the 2nd, 4th, 6th, etc. Same as :nth-child(2n).
  • :nth-child(3n) -- Selects every 3rd child (3, 6, 9, ...).
  • :nth-child(3n+1) -- Selects 1st, 4th, 7th, 10th, etc.
  • :nth-child(-n+3) -- Selects the first 3 children only.
  • :nth-child(n+4) -- Selects everything from the 4th child onward.

Example: Zebra Striping and Advanced nth-child Patterns

/* Zebra-striped table rows */
tbody tr:nth-child(odd) {
    background-color: #f9f9f9;
}

tbody tr:nth-child(even) {
    background-color: #ffffff;
}

/* Every 3rd item gets a special color */
.gallery-item:nth-child(3n) {
    border-color: #e74c3c;
}

/* First 3 items (featured items) */
.product-list li:nth-child(-n+3) {
    font-weight: bold;
    border-left: 3px solid #f39c12;
}

/* All items from the 4th onward */
.product-list li:nth-child(n+4) {
    opacity: 0.8;
}

/* Select items 3 through 6 */
.list-item:nth-child(n+3):nth-child(-n+6) {
    background-color: #eaf2f8;
}
Tip: You can combine two :nth-child() selectors on the same element to create a range. For example, :nth-child(n+3):nth-child(-n+6) selects children 3, 4, 5, and 6. Think of n+3 as "from the 3rd onward" and -n+6 as "up to the 6th."

:nth-last-child(n)

The :nth-last-child(n) pseudo-class works exactly like :nth-child(n) but counts from the end instead of the beginning.

Example: Counting from the End

/* Last 2 items in a list */
.todo-list li:nth-last-child(-n+2) {
    color: #95a5a6;
    font-style: italic;
}

/* Second-to-last item */
.breadcrumb li:nth-last-child(2) {
    font-weight: bold;
}

/* Style based on total sibling count:
   Select the first child only if there are exactly 3 children */
li:first-child:nth-last-child(3) {
    color: green;
}

:nth-of-type(n), :first-of-type, and :last-of-type

While :nth-child counts all siblings regardless of type, :nth-of-type only counts siblings of the same element type. Similarly, :first-of-type and :last-of-type select the first and last sibling of a given type.

This distinction is critical. Consider a container with mixed elements:

Example: nth-of-type vs nth-child

/* HTML:
<div class="content">
    <h2>Title</h2>
    <p>First paragraph</p>
    <p>Second paragraph</p>
    <img src="photo.jpg" alt="Photo">
    <p>Third paragraph</p>
</div>
*/

/* This selects the FIRST <p> element (regardless of other sibling types) */
.content p:first-of-type {
    font-size: 1.25em;
    font-weight: 500;
}

/* This would NOT work as expected:
   p:first-child looks for a <p> that is the first child.
   But the first child is an <h2>, so nothing is selected! */
.content p:first-child {
    /* This selects NOTHING in the above HTML */
}

/* Every other paragraph (1st, 3rd, 5th...) */
.content p:nth-of-type(odd) {
    background-color: #f0f0f0;
}

/* The last paragraph */
.content p:last-of-type {
    margin-bottom: 0;
}
Note: Use :nth-of-type when your container has mixed element types and you want to count only elements of a specific type. Use :nth-child when all siblings are the same type (like <li> inside a <ul>) or when you want to count regardless of type.

:only-child and :only-of-type

The :only-child pseudo-class selects an element that is the sole child of its parent. The :only-of-type selects an element that is the only sibling of its type.

Example: Styling Lone Elements

/* If there is only one item in the list, style it differently */
.notification-list li:only-child {
    text-align: center;
    font-style: italic;
    color: #7f8c8d;
}

/* If there is only one <p> inside an article */
article p:only-of-type {
    font-size: 1.1em;
    line-height: 1.8;
}

/* Hide navigation arrows if there is only one slide */
.carousel-slide:only-child ~ .carousel-arrows {
    display: none;
}

The :root Pseudo-Class

The :root pseudo-class matches the root element of the document -- in HTML, this is always the <html> element. It has higher specificity than the html type selector, making it ideal for defining CSS custom properties (variables) and global defaults.

Example: CSS Custom Properties with :root

:root {
    /* Color palette */
    --color-primary: #3498db;
    --color-secondary: #2ecc71;
    --color-danger: #e74c3c;
    --color-warning: #f39c12;
    --color-text: #2c3e50;
    --color-bg: #ffffff;

    /* Typography */
    --font-sans: 'Inter', 'Segoe UI', sans-serif;
    --font-mono: 'Fira Code', monospace;
    --font-size-base: 16px;
    --line-height-base: 1.6;

    /* Spacing */
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 32px;
    --spacing-xl: 64px;

    /* Border radius */
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 16px;
    --radius-full: 9999px;
}

/* Usage */
body {
    font-family: var(--font-sans);
    color: var(--color-text);
    background-color: var(--color-bg);
    line-height: var(--line-height-base);
}

.btn-primary {
    background-color: var(--color-primary);
    border-radius: var(--radius-md);
    padding: var(--spacing-sm) var(--spacing-md);
}

The :empty Pseudo-Class

The :empty pseudo-class selects elements that have no children -- no child elements, no text content, and no whitespace. This is useful for hiding empty containers or providing fallback content.

Example: Handling Empty Elements

/* Hide empty paragraphs */
p:empty {
    display: none;
}

/* Show a message when a list has no items */
.todo-list:empty::before {
    content: 'No tasks yet. Add your first task!';
    color: #95a5a6;
    font-style: italic;
    display: block;
    padding: 20px;
    text-align: center;
}

/* Hide borders on empty table cells */
td:empty {
    border: none;
}

/* Remove spacing from empty divs */
.container > div:empty {
    margin: 0;
    padding: 0;
}

The :not() Negation Pseudo-Class

The :not() pseudo-class selects every element that does not match the given selector. It is incredibly useful for applying styles broadly and then excluding specific elements.

Example: The Negation Selector in Action

/* All paragraphs except those with class "intro" */
p:not(.intro) {
    font-size: 1rem;
}

/* All inputs except checkboxes and radios */
input:not([type="checkbox"]):not([type="radio"]) {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
}

/* Add a bottom border to all list items except the last one */
li:not(:last-child) {
    border-bottom: 1px solid #eee;
}

/* Style all links that do not have the "btn" class */
a:not(.btn) {
    color: #3498db;
    text-decoration: underline;
}

/* All direct children of .grid that are not hidden */
.grid > *:not(.hidden) {
    display: block;
}
Tip: The pattern li:not(:last-child) is one of the most practical uses of :not(). Instead of adding a border to every list item and then removing it from the last one, you simply exclude the last child from the start. Cleaner and more maintainable.

The :lang() Pseudo-Class

The :lang() pseudo-class selects elements based on the language defined via the lang attribute in the HTML. This is useful for applying language-specific typography or spacing rules.

Example: Language-Specific Styling

/* Arabic text -- right-to-left and different font */
:lang(ar) {
    direction: rtl;
    font-family: 'Noto Sans Arabic', 'Tahoma', sans-serif;
}

/* Chinese text -- different line height for readability */
:lang(zh) {
    line-height: 1.8;
}

/* Japanese quotation marks */
:lang(ja) {
    quotes: '\300C' '\300D' '\300E' '\300F';
}

/* English quotation marks */
:lang(en) {
    quotes: '\201C' '\201D' '\2018' '\2019';
}

The :target Pseudo-Class

The :target pseudo-class selects the element whose id matches the current URL fragment (the part after the # in the URL). For example, if the URL is page.html#section2, then the element with id="section2" matches :target.

Example: Highlighting Targeted Sections and Creating CSS-Only Tabs

/* Highlight the targeted section */
section:target {
    background-color: #fef9e7;
    border-left: 4px solid #f39c12;
    padding-left: 16px;
}

/* Smooth scroll with highlighted target */
html {
    scroll-behavior: smooth;
}

:target {
    animation: highlight-fade 2s ease;
}

@keyframes highlight-fade {
    from { background-color: #f9e79f; }
    to { background-color: transparent; }
}

/* CSS-only tab system using :target */
.tab-content {
    display: none;
}

.tab-content:target {
    display: block;
}

Form Pseudo-Classes

Form pseudo-classes allow you to style form elements based on their current state. These are essential for creating intuitive form experiences with real-time visual feedback.

:enabled and :disabled

These pseudo-classes target form elements based on whether they are enabled or disabled.

:checked

The :checked pseudo-class applies to checkboxes and radio buttons that are currently selected.

:required and :optional

These target form fields based on whether the required attribute is present.

:valid and :invalid

These apply based on whether the form field's value satisfies its validation constraints (like type="email", pattern, min, max, etc.).

:placeholder-shown

The :placeholder-shown pseudo-class applies when the placeholder text is currently visible -- meaning the input is empty.

Example: Comprehensive Form Styling with Pseudo-Classes

/* Disabled fields */
input:disabled,
select:disabled,
textarea:disabled {
    background-color: #ecf0f1;
    cursor: not-allowed;
    opacity: 0.7;
}

/* Enabled fields */
input:enabled {
    background-color: #fff;
}

/* Required field indicator */
input:required {
    border-left: 3px solid #e74c3c;
}

/* Optional field indicator */
input:optional {
    border-left: 3px solid #95a5a6;
}

/* Valid input */
input:valid {
    border-color: #2ecc71;
}

input:valid + .validation-icon::after {
    content: '\2713';
    color: #2ecc71;
}

/* Invalid input */
input:invalid {
    border-color: #e74c3c;
}

input:invalid + .validation-icon::after {
    content: '\2717';
    color: #e74c3c;
}

/* Custom checkbox using :checked */
input[type="checkbox"]:checked + label {
    color: #2ecc71;
    text-decoration: line-through;
}

/* Toggle switch using :checked */
.toggle-input:checked + .toggle-slider {
    background-color: #2ecc71;
}

.toggle-input:checked + .toggle-slider::before {
    transform: translateX(24px);
}

/* Floating label pattern using :placeholder-shown */
.float-label input:placeholder-shown + label {
    top: 50%;
    font-size: 1rem;
    color: #95a5a6;
}

.float-label input:not(:placeholder-shown) + label,
.float-label input:focus + label {
    top: 0;
    font-size: 0.75rem;
    color: #3498db;
}
Note: The :valid and :invalid pseudo-classes apply as soon as the page loads, which can show error styling on empty fields before the user has had a chance to fill them in. To avoid this, combine them with :not(:placeholder-shown) so validation styles only appear after the user has entered something, like: input:invalid:not(:placeholder-shown).

Combining Pseudo-Classes

One of the most powerful aspects of pseudo-classes is that you can chain multiple pseudo-classes on a single selector. This lets you target very specific scenarios.

Example: Chaining Multiple Pseudo-Classes

/* First child that is also hovered */
li:first-child:hover {
    background-color: #eaf2f8;
}

/* Required inputs that are currently invalid and not showing placeholder */
input:required:invalid:not(:placeholder-shown) {
    border-color: #e74c3c;
    background-color: #fdf2f2;
}

/* Last child of type paragraph that is not empty */
p:last-of-type:not(:empty) {
    margin-bottom: 0;
}

/* Odd table rows that are hovered */
tr:nth-child(odd):hover {
    background-color: #d4efdf;
}

/* First-of-type link within a nav that is focused */
nav a:first-of-type:focus-visible {
    outline: 3px solid #f39c12;
}

Exercise 1: Interactive Navigation Menu

Create a vertical navigation menu with the following requirements:

  • Use an unordered list (<ul>) with at least 6 <li> items, each containing an <a> link.
  • Style links with proper LVHA order (:link, :visited, :hover, :active).
  • Give the first item a special "featured" style using :first-child.
  • Add a bottom border to all items except the last one using :not(:last-child).
  • Apply zebra-striping using :nth-child(odd) and :nth-child(even).
  • Use :focus-visible so keyboard users see a clear focus ring but mouse users do not.
  • Add smooth transitions on hover (0.3s ease).

Challenge: Use :focus-within on the <ul> to add a subtle box-shadow around the entire menu when any link inside it has focus.

Exercise 2: Smart Form with Real-Time Feedback

Build a registration form with the following pseudo-class-driven features:

  • An email input (type="email") and a password input (type="password" with minlength="8"), both marked as required.
  • An optional phone number input and an optional "About me" textarea.
  • Style :required fields with a red left border and :optional fields with a grey left border.
  • Show green borders on :valid inputs and red borders on :invalid inputs -- but only after the user has typed something (use :not(:placeholder-shown)).
  • Use :focus-within on each form group to highlight it when the user is typing.
  • Add a checkbox "I agree to terms" and style the label text with a line-through when :checked.
  • Disable the submit button and style it with :disabled.
  • Use :placeholder-shown to create a floating label effect.

Summary

In this lesson, you learned about the full range of CSS pseudo-classes. Here are the key takeaways:

  • Interactive pseudo-classes (:hover, :focus, :active, :visited, :focus-within, :focus-visible) respond to user actions and are essential for interactivity and accessibility.
  • The LVHA order (Link, Visited, Hover, Active) must be followed when styling links to avoid cascade conflicts.
  • Structural pseudo-classes (:first-child, :last-child, :nth-child(), :nth-of-type(), etc.) select elements by position without adding extra classes.
  • :nth-child(an+b) is extremely versatile -- you can select odd/even items, every Nth item, ranges, and more.
  • :not() lets you exclude elements and is one of the most commonly used pseudo-classes in production CSS.
  • Form pseudo-classes (:checked, :valid, :invalid, :required, :placeholder-shown, etc.) enable rich form experiences with CSS alone.
  • :root is the standard place to define CSS custom properties (variables).
  • Pseudo-classes can be chained together for highly specific targeting.

ES
Edrees Salih
12 hours ago

We are still cooking the magic in the way!