Pseudo-class vs Pseudo-element

These two look similar but do very different things:

TypeSyntaxWhat it doesExamples
Pseudo-classSingle colon :Selects elements in a state or position:hover, :focus, :nth-child()
Pseudo-elementDouble colon ::Selects a part of an element or inserts virtual content::before, ::after, ::placeholder

Note: Browsers accept single colon for pseudo-elements (:before) for backwards compatibility, but the modern standard uses double colon (::before). Always use :: in new code.

Common Pseudo-classes

:hover

Element is being hovered by the mouse pointer.

:focus

Element has keyboard or programmatic focus.

:active

Element is being clicked/pressed.

:visited

Link that the user has already visited.

:link

Link that has NOT been visited.

:not(selector)

Every element that does NOT match the selector.

/* Hover effect on button */
.btn {
  background: #6d28d9;
  transition: background 0.2s, transform 0.15s;
}
.btn:hover {
  background: #5b21b6;
  transform: translateY(-2px);
}

/* Visible focus for accessibility */
.btn:focus-visible {
  outline: 3px solid #a78bfa;
  outline-offset: 2px;
}

/* Style unvisited vs visited links */
a:link    { color: #6d28d9; }
a:visited { color: #9333ea; }

/* Every li except the last one */
li:not(:last-child) {
  border-bottom: 1px solid #e0ddf7;
}

Structural Pseudo-classes

These select elements based on their position among siblings.

:first-child, :last-child, :only-child

/* First list item */
li:first-child { font-weight: 700; }

/* Last table row */
tr:last-child td { border-bottom: none; }

/* Card that is an only child */
.card:only-child { margin: 0 auto; }

:nth-child() and :nth-of-type()

These accept a formula An+B where A is the step and B is the offset.

/* Every even row — table striping */
tr:nth-child(even) { background: #f8f7ff; }

/* Every 3rd item starting from the 1st */
li:nth-child(3n+1) { color: #6d28d9; }

/* Odd rows */
tr:nth-child(odd) { background: #fff; }

/* Last 2 items */
li:nth-last-child(-n+2) { opacity: 0.5; }

/* 3rd paragraph specifically */
p:nth-of-type(3) { font-size: 1.1rem; font-weight: 600; }

Keyword shortcuts: :nth-child(even) = :nth-child(2n), :nth-child(odd) = :nth-child(2n+1).

Form Pseudo-classes

These are especially useful for form validation UI without any JavaScript.

/* Input that has been filled */
input:not(:placeholder-shown) {
  border-color: #6d28d9;
}

/* Valid input */
input:valid {
  border-color: #22c55e;
  background: #f0fdf4;
}

/* Invalid input (only after user interaction) */
input:invalid:not(:placeholder-shown) {
  border-color: #ef4444;
  background: #fef2f2;
}

/* Disabled state */
input:disabled {
  background: #f1f5f9;
  cursor: not-allowed;
  opacity: 0.6;
}

/* Checked checkbox */
input[type="checkbox"]:checked + label {
  font-weight: 700;
  color: #6d28d9;
}

/* Required fields */
input:required {
  border-left: 3px solid #f59e0b;
}

Pseudo-elements

::before

Inserts content before the element's content.

::after

Inserts content after the element's content.

::placeholder

Styles the placeholder text of an input.

::selection

Styles text selected by the user.

::first-line

Styles the first line of a block element.

::first-letter

Styles the first letter (drop cap effect).

/* Custom placeholder */
input::placeholder {
  color: #94a3b8;
  font-style: italic;
}

/* Selected text color */
::selection {
  background: #6d28d9;
  color: #fff;
}

/* Drop cap */
article p:first-of-type::first-letter {
  font-size: 3em;
  font-weight: 900;
  float: left;
  line-height: 1;
  margin-right: 0.1em;
  color: #6d28d9;
}

::before and ::after in Depth

These are the most powerful pseudo-elements. They inject a virtual child element before or after the element's real content. The content property is required (even if empty).

/* Decorative quotation marks */
blockquote::before {
  content: '\201C'; /* " */
  font-size: 4em;
  color: #6d28d9;
  line-height: 0;
  vertical-align: -0.4em;
  margin-right: 0.1em;
}

/* Badge / counter on icon */
.cart-icon {
  position: relative;
  display: inline-block;
}
.cart-icon::after {
  content: attr(data-count); /* reads HTML attribute */
  position: absolute;
  top: -6px;
  right: -8px;
  background: #ef4444;
  color: #fff;
  font-size: .65rem;
  font-weight: 700;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Gradient underline on heading */
.fancy-heading::after {
  content: '';
  display: block;
  width: 60px;
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg, #6d28d9, #f59e0b);
  margin-top: 0.5rem;
}

/* Custom checkbox */
.custom-check input:checked + label::before {
  content: '\2713'; /* ✓ */
  background: #6d28d9;
  color: #fff;
}

Trick: Use content: attr(data-x) to read any HTML attribute value directly into a pseudo-element — great for tooltips and badges.

Chaining and Combining

Pseudo-classes can be chained together and combined with pseudo-elements.

/* Last child that is also hovered */
li:last-child:hover { background: #f0edfb; }

/* Not the first child, not disabled */
button:not(:first-child):not(:disabled) { margin-left: .5rem; }

/* ::before on hovered element */
.nav-link:hover::before {
  content: '';
  display: block;
  width: 100%;
  height: 2px;
  background: #6d28d9;
}

/* nth-child with pseudo-element */
li:nth-child(odd)::before {
  content: '▶ ';
  color: #6d28d9;
  font-size: .7em;
}

Practical Patterns

Animated underline on links

.link {
  position: relative;
  text-decoration: none;
  color: #6d28d9;
}
.link::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: #6d28d9;
  transition: width 0.25s ease;
}
.link:hover::after { width: 100%; }

Zebra striped table without JS

table tr:nth-child(even) td { background: #f8f7ff; }
table tr:hover td { background: #ede9fe; }

Input floating label (CSS-only)

.field { position: relative; }
.field label {
  position: absolute;
  top: 50%;
  left: 1rem;
  transform: translateY(-50%);
  transition: all 0.2s;
  color: #94a3b8;
  pointer-events: none;
}
.field input:focus + label,
.field input:not(:placeholder-shown) + label {
  top: 0;
  font-size: .72rem;
  color: #6d28d9;
  background: #fff;
  padding: 0 .25rem;
}