Псевдоклассы

Псевдоклассы — ключевые слова, добавляемые к селектору через :, которые выбирают элемент в определённом состоянии (наведение, фокус, первый потомок, валидный инпут и т.д.) без дополнительных классов в HTML.

Зачем нужно

Псевдоклассы позволяют стилизовать элементы на основе их состояния, позиции в DOM или пользовательского взаимодействия. Без них пришлось бы добавлять классы через JavaScript для каждого hover, focus, nth-child.

Где используется

  • Hover/focus эффекты на кнопках и ссылках
  • Стилизация чётных/нечётных строк таблиц
  • Валидация форм (:valid, :invalid)
  • Выбор первого/последнего элемента в списке
  • Состояния чекбоксов и радиокнопок

Предпосылки

Интерактивные псевдоклассы

:hover — наведение курсора

.button:hover {
  background-color: #0056b3;
  cursor: pointer;
}

.link:hover {
  text-decoration: underline;
  color: #0056b3;
}

:active — момент клика

.button:active {
  transform: scale(0.98);
  background-color: #004085;
}

:focus — элемент в фокусе

/* Для всех фокусируемых элементов */
input:focus {
  outline: 2px solid #007bff;
  outline-offset: 2px;
  border-color: #007bff;
}

a:focus {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

:focus-visible — фокус только с клавиатуры

/* Показывать outline ТОЛЬКО при навигации клавиатурой */
.button:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

/* Убрать outline при клике мышкой */
.button:focus:not(:focus-visible) {
  outline: none;
}

Используйте :focus-visible вместо :focus — это лучший UX. Outline виден при Tab-навигации, но не мешает при клике мышкой.

:focus-within — фокус внутри контейнера

.search-box:focus-within {
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}

/* Работает когда любой потомок в фокусе */
.form-group:focus-within label {
  color: #007bff;
}

Структурные псевдоклассы

:first-child / :last-child

li:first-child {
  border-top: none;
}

li:last-child {
  border-bottom: none;
}

:nth-child

/* Чётные */
tr:nth-child(even) {
  background: #f8f9fa;
}

/* Нечётные */
tr:nth-child(odd) {
  background: white;
}

/* Каждый третий */
li:nth-child(3n) {
  color: #007bff;
}

/* Первые 3 элемента */
li:nth-child(-n + 3) {
  font-weight: bold;
}

/* Начиная с 4-го */
li:nth-child(n + 4) {
  opacity: 0.7;
}

/* Конкретный элемент */
li:nth-child(2) {
  color: red;
}

:nth-last-child

/* Последние 2 элемента */
li:nth-last-child(-n + 2) {
  border-bottom: none;
}

/* Предпоследний */
li:nth-last-child(2) {
  font-style: italic;
}

:nth-of-type

/* Каждый второй параграф (не элемент, а именно <p>) */
p:nth-of-type(even) {
  margin-left: 2rem;
}

/* Первый <h2> в контейнере */
h2:first-of-type {
  margin-top: 0;
}

/* Последний <img> */
img:last-of-type {
  margin-bottom: 0;
}

:only-child / :only-of-type

/* Единственный потомок */
li:only-child {
  list-style: none;
}

/* Единственный <p> среди разных элементов */
p:only-of-type {
  font-size: 1.25rem;
}

:empty

/* Пустые элементы */
.container:empty {
  display: none;
}

/* Плейсхолдер для пустого состояния */
.list:empty::before {
  content: "Список пуст";
  color: #6c757d;
}

Псевдоклассы форм

:checked

/* Стилизация чекбоксов и радио */
input[type="checkbox"]:checked + label {
  color: #28a745;
  text-decoration: line-through;
}

/* Кастомный toggle */
input[type="checkbox"]:checked + .toggle {
  background: #007bff;
}

input[type="checkbox"]:checked + .toggle::before {
  transform: translateX(24px);
}

:disabled / :enabled

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: #6c757d;
}

input:disabled {
  background: #e9ecef;
  color: #6c757d;
}

input:enabled {
  border-color: #ced4da;
}

:valid / :invalid

input:valid {
  border-color: #28a745;
}

input:invalid {
  border-color: #dc3545;
}

/* Только после взаимодействия пользователя */
input:not(:placeholder-shown):valid {
  border-color: #28a745;
}

input:not(:placeholder-shown):invalid {
  border-color: #dc3545;
}

:required / :optional

input:required {
  border-left: 3px solid #dc3545;
}

input:optional {
  border-left: 3px solid #ced4da;
}

:placeholder-shown

/* Когда placeholder виден (инпут пустой) */
input:placeholder-shown {
  border-color: #ced4da;
}

/* Floating label */
.input-group input:placeholder-shown + label {
  transform: translateY(0);
  font-size: 1rem;
  color: #6c757d;
}

.input-group input:not(:placeholder-shown) + label {
  transform: translateY(-1.5rem);
  font-size: 0.75rem;
  color: #007bff;
}

:read-only / :read-write

input:read-only {
  background: #e9ecef;
  cursor: default;
}

input:read-write {
  background: white;
}

Отрицание и логика

:not

/* Все кнопки, кроме disabled */
button:not(:disabled) {
  cursor: pointer;
}

/* Все <li>, кроме последнего */
li:not(:last-child) {
  border-bottom: 1px solid #dee2e6;
}

/* Несколько исключений */
a:not(.nav-link):not(.button) {
  text-decoration: underline;
}

Другие полезные псевдоклассы

:root

:root {
  --primary: #007bff;
  --text: #333;
  font-size: 100%;
}

:target

/* Элемент, на который ведёт якорная ссылка (#id) */
:target {
  scroll-margin-top: 80px;
  animation: highlight 1s ease;
}

:lang

:lang(ru) {
  quotes: "\00AB" "\00BB"; /* « » */
}

:lang(en) {
  quotes: "\201C" "\201D"; /* " " */
}

Частые ошибки

  1. :hover на мобильных — «залипает» после тапа. Используйте @media (hover: hover):
    @media (hover: hover) {
      .button:hover { background: #0056b3; }
    }
    
  2. :nth-child vs :nth-of-typenth-child(2) — второй потомок любого типа, nth-of-type(2) — второй потомок данного типа
  3. :first-child ожидает тег.item:first-child выбирает .item, только если он первый потомок родителя
  4. :valid показывается сразу — используйте :not(:placeholder-shown):valid для показа после взаимодействия

Практика

  • Стилизовать hover/focus/active состояния кнопки
  • Использовать :nth-child(even/odd) для зебра-полос таблицы
  • Создать floating label через :placeholder-shown
  • Стилизовать валидацию формы (:valid, :invalid)
  • Использовать :focus-visible вместо :focus

Связанные темы

Ресурсы