CSS: Pseudo-classes
Target elements based on their state, position, or relationship to other elements.
:hover and :active
:hover applies styles when the user moves their mouse over an element. :active applies while the element is being clicked or pressed. Both are commonly used on buttons and links.
:hover and :active
:hover and :active give visual feedback to the user. Always provide hover and active states on interactive elements like buttons and links.
- :hovermouse is over the element
- :activeelement is being clicked (held down)
- :hover does not work on touch devices, use :focus-visible for keyboard users
- Stack them in order: :link, :visited, :hover, :active (LoVe HAte mnemonic)
:hover and :active
Button changes colour on hover and shrinks slightly when clicked.
.btn {
background: steelblue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.btn:hover { background: #1d6fa4; }
.btn:active { background: #164e75; transform: scale(0.97); }:focus
:focus applies when an element receives focus, either by clicking on it or by tabbing to it with the keyboard. It is critical for keyboard accessibility.
:focus
Never remove the default focus outline without replacing it with a visible alternative. Keyboard users depend on it to know which element is active.
- :focuselement is focused via click or keyboard Tab
- :focus-visiblefocus ring shows only for keyboard navigation, not mouse clicks (modern best practice)
- :focus-withinapplies to a parent when any of its children are focused
- Never use outline: none on :focus without providing a visible replacement
- Use :focus-visible to avoid showing focus rings on mouse users while keeping them for keyboard users
:focus
A custom focus ring appears when the input or button receives keyboard focus.
.field, .btn {
padding: 8px 12px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 1rem;
outline: none;
margin-right: 8px;
}
.field:focus,
.btn:focus-visible {
border-color: steelblue;
box-shadow: 0 0 0 3px rgba(70, 130, 180, 0.35);
}
.btn { background: steelblue; color: white; cursor: pointer; }:visited and :link
:link targets anchor elements that have not been visited yet. :visited targets links the user has already opened. Together they let you style links differently based on browsing history.
:link and :visited
For privacy reasons, :visited can only change color, background-color, border-color, and outline-color. JavaScript cannot read :visited state.
- :linkunvisited anchor tag with an href
- :visitedanchor tag the user has already followed
- Correct order: :link then :visited (before :hover and :active)
- Privacy restriction: only color-related properties work on :visited
:link and :visited
Unvisited links are blue; visited links turn grey.
.nav-link { color: steelblue; text-decoration: none; display: block; margin-bottom: 8px; }
.nav-link:link { color: steelblue; }
.nav-link:visited { color: #888; }
.nav-link:hover { text-decoration: underline; }:first-child and :last-child
:first-child selects an element that is the first child of its parent. :last-child selects the last child. These are useful for removing borders or margins from the first or last item in a list.
:first-child and :last-child
Use :first-child and :last-child to remove unwanted spacing or borders at the edges of a list or group without adding extra classes.
- li:first-childthe first li inside its parent
- li:last-childthe last li inside its parent
- :first-of-typethe first element of that tag type inside its parent (ignores siblings of other types)
- :last-of-typethe last element of that tag type inside its parent
- Common pattern: add a border-bottom to all li items, then remove it from :last-child
:first-child and :last-child
Border removed from the last item; first item has a distinct background.
.list { list-style: none; padding: 0; margin: 0; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
.list li {
padding: 10px 14px;
border-bottom: 1px solid #ddd;
}
.list li:first-child { background: #eff6ff; font-weight: bold; }
.list li:last-child { border-bottom: none; }:nth-child() and :nth-of-type()
:nth-child() selects elements by their position among siblings. You can pass a number, the keywords odd / even, or a formula like 3n+1.
:nth-child()
:nth-child() counts all sibling elements regardless of type. :nth-of-type() counts only siblings of the same element type.
- :nth-child(1)the first child
- :nth-child(odd)children 1, 3, 5, 7 …
- :nth-child(even)children 2, 4, 6, 8 …
- :nth-child(3n)every third child: 3, 6, 9 …
- :nth-child(3n+1)children 1, 4, 7, 10 …
- :nth-of-type(2)the second element of that tag type inside its parent
:nth-child(even)
Alternating row colours using nth-child, a classic zebra-stripe table.
.tbl { border-collapse: collapse; width: 100%; }
.tbl tr td {
padding: 10px 14px;
border: 1px solid #ddd;
}
.tbl tr:nth-child(even) td { background: #f0f4f8; }
.tbl tr:nth-child(odd) td { background: white; }
.tbl tr:hover td { background: #dbeafe; }:not() Selector
:not(selector) selects every element that does not match the given selector. It is useful for applying styles to all items except one specific case.
:not()
:not() is a negation pseudo-class. It accepts any valid selector. Modern CSS also supports a comma-separated list inside :not() to exclude multiple selectors at once.
- li:not(:last-child)all li items except the last one
- input:not([type='submit'])all inputs except submit buttons
- :not(.active)all elements without the active class
- :not(.a, .b)excludes elements matching either .a or .b (modern CSS)
- The specificity of :not() is determined by its argument, not by :not() itself
:not(:last-child)
Border-bottom added to all items except the last, no extra class needed.
.list { list-style: none; padding: 0; margin: 0; }
.list li {
padding: 10px 14px;
}
/* Add divider to every item except the last */
.list li:not(:last-child) {
border-bottom: 1px solid #ddd;
}:checked, :disabled, :enabled
These pseudo-classes target form elements based on their interactive state. They are essential for building custom-styled form controls that still respond to native HTML behaviour.
Form state pseudo-classes
These pseudo-classes reflect the live state of a form control. They update automatically when the user interacts with the form.
- :checkedcheckbox or radio that is currently checked
- :disabledinput, button, or select with the disabled attribute
- :enabledinput or control that is not disabled
- :read-onlyinput with the readonly attribute
- :placeholder-showninput whose placeholder text is currently visible
:checked, :disabled
Checked checkbox gets a highlight; disabled input is visually greyed out.
.checkbox-label { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; cursor: pointer; }
.checkbox-label input:checked + * { color: steelblue; font-weight: bold; }
.field {
display: block;
padding: 8px 12px;
border: 2px solid #ccc;
border-radius: 4px;
margin-bottom: 8px;
font-size: 1rem;
}
.field:enabled { border-color: #ccc; background: white; }
.field:disabled { background: #f3f4f6; color: #9ca3af; cursor: not-allowed; border-color: #e5e7eb; }:valid and :invalid
:valid and :invalid reflect whether an input's current value passes the HTML constraint validation rules (such as required, type="email", or minlength).
:valid and :invalid
:invalid fires immediately on page load for empty required fields. Use :not(:placeholder-shown):invalid to only show the error state after the user has started interacting.
- :validinput value passes all validation constraints
- :invalidinput value fails one or more constraints
- :requiredinput has the required attribute
- :optionalinput does not have the required attribute
- input:not(:placeholder-shown):invalidinvalid only when the user has typed something
:valid and :invalid
Border turns green for a valid email and red for an invalid one.
.email {
display: block;
padding: 8px 12px;
border: 2px solid #ccc;
border-radius: 4px;
font-size: 1rem;
outline: none;
width: 260px;
}
/* Only show error after user has typed something */
.email:not(:placeholder-shown):invalid { border-color: crimson; background: #fff5f5; }
.email:not(:placeholder-shown):valid { border-color: seagreen; background: #f0fdf4; }:is(), :where(), :has() (Modern)
These are newer pseudo-classes that make selectors more powerful and concise. They are now well supported in all modern browsers.
:is(), :where(), :has()
These three pseudo-classes group selectors, reduce repetition, and unlock parent selection, previously impossible in CSS.
- :is(h1, h2, h3)matches any of the listed selectors; takes the highest specificity of its arguments
- :where(h1, h2, h3)same as :is() but always has zero specificity, safer for resets and utilities
- :has(img)selects a parent that contains a matching child (the 'parent selector')
- :has(:checked)selects a label when its checkbox is checked
- :is() and :where() reduce repetitive selector lists; :has() enables patterns previously requiring JavaScript
:has(img)
The card that contains an image gets a different border colour via :has().
.card {
border: 2px solid #ccc;
border-radius: 6px;
padding: 16px;
margin-bottom: 12px;
max-width: 320px;
}
/* Target the card that HAS an img inside it */
.card:has(img) {
border-color: steelblue;
padding: 0;
overflow: hidden;
}
.card:has(img) img { width: 100%; display: block; }
.card:has(img) h2,
.card:has(img) p { padding: 0 14px; }
.card:has(img) h2 { margin-top: 10px; }
/* :is() grouping example */
:is(.card h2, .card h3) { font-size: 1.1rem; margin-bottom: 6px; }Knowledge Check
1. Which pseudo-class applies while the user hovers the mouse over an element?
2. What does :nth-child(2n) select?
3. Which pseudo-class selects an element only when it does NOT match a given selector?
4. What is the difference between :first-child and :first-of-type?
5. Which pseudo-class applies styles when an input's value passes HTML validation?
6. What does :has() allow you to do?