Псевдоклассы
Псевдоклассы — ключевые слова, добавляемые к селектору через
:, которые выбирают элемент в определённом состоянии (наведение, фокус, первый потомок, валидный инпут и т.д.) без дополнительных классов в 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"; /* " " */
}
Частые ошибки
:hoverна мобильных — «залипает» после тапа. Используйте@media (hover: hover):@media (hover: hover) { .button:hover { background: #0056b3; } }:nth-childvs:nth-of-type—nth-child(2)— второй потомок любого типа,nth-of-type(2)— второй потомок данного типа:first-childожидает тег —.item:first-childвыбирает.item, только если он первый потомок родителя:validпоказывается сразу — используйте:not(:placeholder-shown):validдля показа после взаимодействия
Практика
- Стилизовать hover/focus/active состояния кнопки
- Использовать
:nth-child(even/odd)для зебра-полос таблицы - Создать floating label через
:placeholder-shown - Стилизовать валидацию формы (
:valid,:invalid) - Использовать
:focus-visibleвместо:focus
Связанные темы
- Псевдоэлементы —
::before,::after - Функциональные селекторы —
:is,:where,:has - Каскад и специфичность — вес псевдоклассов
- transition -- плавные переходы — анимация состояний