CSS hover эффект при наведении – рабочие примеры CSS и HTML
CSS hover эффект при наведении меняет состояние элемента, пока указатель находится над ним. Скопируйте первый пример, затем выберите прием для кнопки, изображения, иконки или карточки. В каждом примере эффекта есть минимальная разметка и CSS, поэтому вы сможете заменить цвет, направление и длительность без JavaScript.
Как работает CSS hover эффект при наведении
Псевдокласс :hover выбирает элемент под указателем. Свойство transition плавно меняет значение между обычным состоянием и состоянием наведения.
<button class='demo' type='button'>Навести указатель</button>
<style>
.demo { padding: 12px 20px; color: white; background: #1746a2; border: 0; transition: background-color .2s ease-out; }
.demo:hover, .demo:focus-visible { background: #102e6c; }
.demo:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
</style>Цвет меняется за .2s с функцией ease-out. Правило перехода стоит в базовом состоянии, поэтому возврат после ухода указателя тоже плавный.
Состояние описывают в CSS, когда отклик не меняет данные. Обработчик клика нужен для действия, а не для обычной подсветки. Для последовательности кадров и повторения применяйте @keyframes. Подробное описание псевдокласса есть в документации MDN о :hover.
Эффекты для кнопок
Кнопочный отклик можно строить на заливке, линии, смещении с тенью или заранее зарезервированной рамке. Выбирайте прием по характеру действия и следите, чтобы фокус и размеры кнопки оставались предсказуемыми.
Заливка слева направо
<button class='fill-btn' type='button'>Добавить</button>
<style>
.fill-btn { position: relative; isolation: isolate; overflow: hidden; padding: 12px 24px; border: 2px solid #1746a2; border-radius: 8px; background: white; color: #1746a2; font: inherit; cursor: pointer; transition: color .2s ease-out; }
.fill-btn::before { content: ''; position: absolute; inset: 0; z-index: -1; background: #1746a2; transform: scaleX(0); transform-origin: left; transition: transform .2s ease-out; }
.fill-btn:hover, .fill-btn:focus-visible { color: white; }
.fill-btn:hover::before, .fill-btn:focus-visible::before { transform: scaleX(1); }
.fill-btn:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .fill-btn, .fill-btn::before { transition: none; } }
</style>Псевдоэлемент раскрывается через scaleX(1), поэтому кнопка не меняет размеры. Замените два цвета, а для движения справа укажите transform-origin: right.
Подчеркивание из ::after
<button class='line-btn' type='button'>Подробнее</button>
<style>
.line-btn { position: relative; padding: 12px 4px; border: 0; background: transparent; color: #1746a2; font: inherit; cursor: pointer; }
.line-btn::after { content: ''; position: absolute; left: 4px; right: 4px; bottom: 4px; height: 2px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .2s ease-out; }
.line-btn:hover::after, .line-btn:focus-visible::after, .line-btn:active::after { transform: scaleX(1); }
.line-btn:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .line-btn::after { transition: none; } }
</style>currentColor берет цвет текста кнопки. Измените transform-origin на center, чтобы линия раскрывалась от середины.
Подъем с тенью
<button class='lift-btn' type='button'>Выбрать</button>
<style>
.lift-btn { position: relative; isolation: isolate; padding: 12px 24px; border: 0; border-radius: 8px; background: #1746a2; color: white; font: inherit; cursor: pointer; transition: transform .2s ease-out; }
.lift-btn::after { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; box-shadow: 0 8px 18px rgb(0 0 0 / .28); opacity: 0; pointer-events: none; transition: opacity .2s ease-out; }
.lift-btn:hover { transform: translateY(-3px); }
.lift-btn:hover::after, .lift-btn:focus-visible::after { opacity: 1; }
.lift-btn:active { transform: translateY(1px); }
.lift-btn:focus-visible { outline: 3px solid #111; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .lift-btn, .lift-btn::after { transition: none; } .lift-btn:hover, .lift-btn:active { transform: none; } }
</style>Кнопка смещается на 3 пикселя, а готовая тень появляется через opacity. Контейнер вокруг кнопки не должен обрезать тень свойством overflow: hidden.
Обводка без скачка размеров
<button class='border-btn' type='button'>Настройки</button>
<style>
.border-btn { box-sizing: border-box; padding: 12px 24px; border: 2px solid transparent; border-radius: 8px; background: #e9efff; color: #12336e; font: inherit; cursor: pointer; transition: border-color .2s ease-out; }
.border-btn:hover, .border-btn:focus-visible, .border-btn:active { border-color: #1746a2; }
.border-btn:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .border-btn { transition: none; } }
</style>Прозрачная рамка заранее занимает 2 пикселя. При наведении меняется только ее цвет, поэтому соседние элементы не сдвигаются.
Эффекты для изображений
Для изображений подходят зум, затемнение, выезжающая подпись и переворот карточки. При выборе сохраняйте видимыми название и ссылку, чтобы смысл и действие не зависели от наведения.
Зум внутри контейнера
<a class='zoom-card' href='#zoom-details'>
<img src='photo.jpg' alt='Горный маршрут' />
<span>Открыть маршрут</span>
</a>
<p id='zoom-details'>Описание горного маршрута.</p>
<style>
.zoom-card { display: block; width: min(100%, 320px); overflow: hidden; border-radius: 12px; color: #12336e; background: #edf3ff; }
.zoom-card img { display: block; width: 100%; height: auto; transition: transform .25s ease-out; }
.zoom-card span { display: block; padding: 12px; background: #edf3ff; }
.zoom-card:hover img, .zoom-card:focus-visible img { transform: scale(1.06); }
.zoom-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .zoom-card img { transition: none; transform: none !important; } }
</style>overflow: hidden обрезает увеличенную картинку внутри карточки. Значение 1.06 дает небольшой зум без изменения ширины блока.
Затемнение оверлеем
<a class='shade-card' href='#shade-details'>
<img src='photo.jpg' alt='Горный маршрут' />
<span>Поход в горы</span>
</a>
<p id='shade-details'>Программа похода и список снаряжения.</p>
<style>
.shade-card { position: relative; display: block; width: min(100%, 320px); overflow: hidden; border-radius: 12px; color: white; }
.shade-card img { display: block; width: 100%; height: auto; }
.shade-card::after { content: ''; position: absolute; inset: 0; background: #000; opacity: .12; pointer-events: none; transition: opacity .2s ease-out; }
.shade-card:hover::after, .shade-card:focus-visible::after { opacity: .4; }
.shade-card span { position: absolute; z-index: 1; bottom: 12px; left: 12px; padding: 8px 12px; background: #102e6c; }
.shade-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .shade-card::after { transition: none; } }
</style>Оверлей меняет прозрачность, а подпись остается в разметке. Поэтому название доступно без наведения, включая сенсорные устройства.
Подпись, выезжающая из-под края
<a class='caption-card' href='#caption-details'>
<span class='caption-viewport'><img src='photo.jpg' alt='Горный маршрут' /><span class='caption-slide' aria-hidden='true'>Посмотреть маршрут</span></span>
<span class='caption-name'>Посмотреть маршрут</span>
</a>
<p id='caption-details'>Описание маршрута доступно по ссылке.</p>
<style>
.caption-card { display: block; width: min(100%, 320px); color: #12336e; }
.caption-viewport { position: relative; display: block; overflow: hidden; }
.caption-viewport img { display: block; width: 100%; height: auto; }
.caption-slide { position: absolute; left: 0; right: 0; bottom: 0; padding: 12px; background: #102e6c; color: white; transform: translateY(100%); transition: transform .2s ease-out; }
.caption-name { display: block; padding: 12px 0; }
.caption-card:hover .caption-slide, .caption-card:focus-visible .caption-slide { transform: translateY(0); }
.caption-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .caption-slide { transition: none; } }
</style>Основное название ссылки всегда видно. Декоративная копия выезжает на 100% своей высоты и не содержит единственную цену или кнопку.
Переворот карточки
<a class='flip-card' href='#flip-details'>
<span class='flip-inner'><span class='flip-front'><img src='photo.jpg' alt='Горный маршрут' /><span>Горный маршрут</span></span><span class='flip-back' aria-hidden='true'>Горный маршрут – открыть описание</span></span>
</a>
<p id='flip-details'>Описание маршрута доступно без переворота карточки.</p>
<style>
.flip-card { display: block; width: min(100%, 320px); height: 200px; perspective: 800px; color: white; }
.flip-inner { position: relative; display: block; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform .3s ease-out; }
.flip-front, .flip-back { position: absolute; inset: 0; display: grid; place-items: center; backface-visibility: hidden; border-radius: 12px; overflow: hidden; }
.flip-front { background: #1746a2; } .flip-front img { width: 100%; height: 100%; object-fit: cover; }
.flip-front span { position: absolute; bottom: 12px; padding: 8px; background: #102e6c; }
.flip-back { padding: 20px; background: #102e6c; transform: rotateY(180deg); }
.flip-card:hover .flip-inner, .flip-card:focus-visible .flip-inner { transform: rotateY(180deg); }
.flip-card:focus-visible { outline: 3px solid #111; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .flip-inner { transition: none; transform: none !important; } }
</style>perspective задает глубину, а rotateY(180deg) показывает обратную сторону. Ссылка должна открывать описание без обязательного переворота карточки.
Эффекты для иконок и SVG
SVG-иконки реагируют через рисующуюся обводку, смену заливки или поворот. Эффект должен оставаться декоративным: название действия задается кнопкой и aria-label, поэтому оно доступно без ориентации на изображение.
Рисующаяся обводка
<button class='draw-icon' type='button' aria-label='Выбрать элемент'>
<svg viewBox='0 0 40 40' aria-hidden='true'><circle class='draw-ring' cx='20' cy='20' r='16' pathLength='100' /><path d='m13 20 5 5 9-10' fill='none' stroke='currentColor' stroke-width='3' /></svg>
</button>
<style>
.draw-icon { padding: 8px; border: 1px solid #aab8d0; border-radius: 8px; background: white; color: #1746a2; cursor: pointer; }
.draw-icon svg { display: block; width: 40px; height: 40px; }
.draw-ring { fill: none; stroke: currentColor; stroke-width: 2; stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset .25s ease-out; }
.draw-icon:hover .draw-ring, .draw-icon:focus-visible .draw-ring, .draw-icon:active .draw-ring { stroke-dashoffset: 0; }
.draw-icon:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .draw-ring { transition: none; } }
</style>Атрибут pathLength='100' упрощает расчет штрихов. Значение stroke-dashoffset: 0 показывает весь контур.
Смена заливки
<button class='fill-icon' type='button' aria-label='Добавить в избранное'>
<svg viewBox='0 0 40 40' aria-hidden='true'><path d='m20 4 5 10 11 2-8 8 2 12-10-6-10 6 2-12-8-8 11-2z' /></svg>
</button>
<style>
.fill-icon { padding: 8px; border: 1px solid #aab8d0; border-radius: 8px; background: white; cursor: pointer; }
.fill-icon svg { display: block; width: 40px; height: 40px; }
.fill-icon path { fill: #dce4f5; stroke: #12336e; transition: fill .2s ease-out; }
.fill-icon:hover path, .fill-icon:focus-visible path, .fill-icon:active path { fill: #1746a2; }
.fill-icon:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .fill-icon path { transition: none; } }
</style>Селектор обращается к конкретному контуру SVG. Наведение показывает отклик, но не сохраняет выбор; постоянное состояние потребует логики кнопки и aria-pressed.
Поворот иконки
<button class='rotate-icon' type='button' aria-label='Открыть настройки'>
<svg viewBox='0 0 40 40' aria-hidden='true'><path d='M20 5v30M5 20h30M9 9l22 22M9 31 31 9' fill='none' stroke='currentColor' stroke-width='5' /></svg>
</button>
<style>
.rotate-icon { padding: 8px; border: 1px solid #aab8d0; border-radius: 8px; background: white; color: #1746a2; cursor: pointer; }
.rotate-icon svg { display: block; width: 40px; height: 40px; transform-origin: center; transition: transform .2s ease-out; }
.rotate-icon:hover svg, .rotate-icon:focus-visible svg { transform: rotate(30deg); }
.rotate-icon:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .rotate-icon svg { transition: none; transform: none !important; } }
</style>Поворот на 30 градусов подходит для декоративного отклика. Название действия остается в aria-label, поэтому смысл не зависит от изображения.
Направление курсора (direction-aware)
Чистый CSS не получает координаты входа указателя. Четыре зоны дают приближение, а точное направление входа и выхода требует JavaScript. CSS-вариант проще поддерживать, JavaScript-вариант добавляет обработчики событий и расчет геометрии.
CSS-вариант с четырьмя зонами
<a class='zones-card' href='#zones-details'>
<span class='zone zone-top' aria-hidden='true'></span><span class='zone zone-right' aria-hidden='true'></span><span class='zone zone-bottom' aria-hidden='true'></span><span class='zone zone-left' aria-hidden='true'></span>
<span class='zones-title'>Открыть проект</span>
</a>
<p id='zones-details'>Описание проекта.</p>
<style>
.zones-card { position: relative; display: grid; place-items: center; width: min(100%, 320px); height: 200px; overflow: hidden; background: #dcecff; color: white; }
.zones-card::after { content: ''; position: absolute; inset: 0; z-index: 1; background: #1746a2; opacity: 0; pointer-events: none; }
.zones-title { position: relative; z-index: 2; padding: 10px; background: #102e6c; pointer-events: none; }
.zone { display: none; position: absolute; inset: 0; z-index: 3; }
.zone-top { clip-path: polygon(0 0, 100% 0, 50% 50%); } .zone-right { clip-path: polygon(100% 0, 100% 100%, 50% 50%); } .zone-bottom { clip-path: polygon(0 100%, 100% 100%, 50% 50%); } .zone-left { clip-path: polygon(0 0, 0 100%, 50% 50%); }
.zones-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
@media (hover: hover) and (pointer: fine) {
@supports selector(:has(*)) {
.zone { display: block; }
.zones-card:has(.zone:hover)::after { opacity: 1; animation: zone-in .2s ease-out; }
.zones-card:has(.zone-top:hover)::after { animation-name: zone-top-in; } .zones-card:has(.zone-right:hover)::after { animation-name: zone-right-in; } .zones-card:has(.zone-bottom:hover)::after { animation-name: zone-bottom-in; } .zones-card:has(.zone-left:hover)::after { animation-name: zone-left-in; }
}
}
@keyframes zone-in { from { transform: translateY(-100%); } to { transform: none; } }
@keyframes zone-top-in { from { transform: translateY(-100%); } to { transform: none; } } @keyframes zone-right-in { from { transform: translateX(100%); } to { transform: none; } } @keyframes zone-bottom-in { from { transform: translateY(100%); } to { transform: none; } } @keyframes zone-left-in { from { transform: translateX(-100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .zones-card::after { animation: none !important; } }
</style>Четыре элемента определяют зону указателя, но CSS не запоминает сторону входа. Условие (hover: hover) and (pointer: fine) оставляет этот прием для точного указателя, а @supports защищает вариант от отсутствия :has().
JavaScript-вариант с ближайшим краем
<a class='direction-card' href='#direction-details'><span class='direction-panel' aria-hidden='true'></span><span class='direction-title'>Открыть проект</span></a>
<p id='direction-details'>Материалы проекта.</p>
<style>
.direction-card { position: relative; display: grid; place-items: center; width: min(100%, 320px); height: 200px; overflow: hidden; background: #dcecff; color: white; }
.direction-panel { position: absolute; inset: 0; background: #1746a2; pointer-events: none; transform: translateY(-101%); transition: transform .2s ease-out; }
.direction-title { position: relative; padding: 10px; background: #102e6c; }
.direction-card:focus-visible { outline: 3px solid #111; outline-offset: 3px; } .direction-card:focus-visible .direction-panel { transform: translate(0, 0) !important; }
@media (prefers-reduced-motion: reduce) { .direction-panel { transition: none !important; } }
</style>
<script>
const cards = document.querySelectorAll('.direction-card');
cards.forEach(card => {
const panel = card.querySelector('.direction-panel');
const edge = event => { const r = card.getBoundingClientRect(); const x = event.clientX - r.left; const y = event.clientY - r.top; const d = [y, r.width - x, r.height - y, x]; const i = d.indexOf(Math.min(...d)); return ['translateY(-101%)','translateX(101%)','translateY(101%)','translateX(-101%)'][i]; };
card.addEventListener('pointerenter', event => { panel.style.transition = 'none'; panel.style.transform = edge(event); panel.getBoundingClientRect(); requestAnimationFrame(() => { panel.style.transition = ''; panel.style.transform = 'translate(0, 0)'; }); });
card.addEventListener('pointerleave', event => { panel.style.transform = edge(event); });
});
</script>Функция сравнивает расстояния до четырех краев карточки. Скрипт запускает панель со стороны входа и возвращает ее к стороне выхода. При отключенном JavaScript ссылка и подпись сохраняют работу.
Что анимировать можно, а что нельзя
Кадр страницы проходит через layout, paint и composite. Берите свойства, которые меняют уже нарисованный элемент, особенно когда эффект занимает крупную область.
| Свойство | Что пересчитывает браузер | Подходит для hover |
|---|---|---|
transform | Обычно composite | Да, для сдвига, зума и поворота |
opacity | Обычно composite | Да, для готового слоя |
width, height | Layout и последующие этапы | Избегайте непрерывного изменения |
top, left, margin | Layout и последующие этапы | Для сдвига берите transform |
box-shadow | Зависит от размера и результата | Используйте на небольших элементах |
background-color, border-color, fill | Paint | Подходит для небольших элементов |
stroke-dashoffset | Paint SVG | Подходит для небольшого SVG |
transform и opacity часто позволяют браузеру обновлять композитный слой без layout и paint. Итог зависит от размера элемента и его окружения.
will-change заранее сообщает браузеру о предстоящем изменении и расходует память. Используйте его только для элемента с найденной проблемой производительности, а не для всех кнопок страницы.
Тач-устройства
Сенсорный экран не дает устойчивого состояния наведения. Основное действие и подпись должны работать без :hover.
<button class='touch-btn' type='button'>Продолжить</button>
<style>
.touch-btn { padding: 12px 24px; border: 2px solid #1746a2; border-radius: 8px; background: white; color: #12336e; font: inherit; cursor: pointer; transition: background-color .2s ease-out; }
@media (hover: hover) and (pointer: fine) { .touch-btn:hover { background: #dcecff; } }
.touch-btn:focus-visible { outline: 3px solid #111; outline-offset: 3px; }
.touch-btn:active { background: #c1d7ff; }
@media (prefers-reduced-motion: reduce) { .touch-btn { transition: none; } }
</style>Медиазапрос оставляет декоративное наведение для точного указателя. Фокус клавиатуры и активное состояние дают отклик при других способах ввода.
Доступность
Наведение дополняет интерфейс. Название, цена и основное действие должны быть доступны до движения указателя.
- Сохраняйте заметный фокус через
:focus-visible. - Берите нативные кнопки и ссылки вместо декоративных
div. - Проверяйте контраст текста во всех состояниях.
- Не прячьте единственную цену или кнопку на обратной стороне карточки.
- Дублируйте hover-отклик состоянием фокуса, если он помогает понять интерфейс.
Медиазапрос prefers-reduced-motion отключает переходы для людей, которые предпочитают уменьшенное движение. Для пространственных эффектов можно убрать движение полностью, сохранив цвет и фокус.
Частые ошибки в CSS hover-эффектах
Ошибки в hover часто связаны с местом размещения transition, попыткой анимировать display и несоответствием области отклика ссылке. Отдельного внимания требуют длительность перехода и необязательные свойства в transition: all.
Transition записан только в :hover
Когда transition стоит только в состоянии наведения, возврат после ухода указателя может стать мгновенным. Перенесите правило в базовое состояние.
.example { background: #1746a2; transition: background-color .2s ease-out; }
.example:hover { background: #102e6c; }Попытка анимировать display
Значения none и block не создают промежуточных кадров. Для декоративного появления меняйте opacity или transform, а настоящие элементы управления дополнительно убирайте из взаимодействия.
Отклик занимает всю карточку, а ссылка только заголовок
Большая подсветка обещает действие на всей области. Если карточка ведет в одно место, сделайте всю карточку ссылкой. При нескольких действиях оставьте отдельные кнопки и ссылки.
Отклик длится слишком долго
Для обычного hover используйте конкретную длительность около .2s. Переходы дольше .3s часто задерживают простой отклик.
Transition: all меняет лишнее
Перечисляйте свойства явно, например transform .2s ease-out, opacity .2s ease-out. Так ширина и отступы не получат случайную анимацию.
Что выбрать под задачу
| Задача | Эффект | Что оставить без hover |
|---|---|---|
| Основная кнопка | Заливка или смена фона | Название действия и фокус |
| Текстовая ссылка | Подчеркивание | Отличие ссылки от текста |
| Карточка товара | Затемнение с подписью | Название, цену и покупку |
| Иллюстрация портфолио | Зум или декоративная подпись | Ссылку на проект |
| Иконка действия | SVG-обводка или смена заливки | Доступное название кнопки |
| Декоративная карточка | Переворот или direction-aware | Понятное статичное состояние |
Совет: сначала добейтесь понятного действия без анимации. Затем добавьте один эффект и проверьте его мышью, клавиатурой и касанием.
Популярное за неделю
- Как создать сайт самому в 2026: 4 способа + пошаговая инструкция 34 мин чтения
- Лучшие конструкторы для создания сайтов — плюсы, минусы и сравнения 22 мин чтения
- overflow hidden что это и почему обрезка не работает 6 мин чтения
- 50 примеров CSS3‑кнопок с эффектами для сайта 5 мин чтения
- 17 эффектных CSS‑анимаций с готовыми примерами 3 мин чтения
- 19 CSS‑спиннеров и прелоадеров для загрузки 5 мин чтения
Комментарии
круто
можно лучше
здорово! просто нет слов! как раз занимаюсь оформлением сайта..буду экспериментировать! Спасибо!
ЭТО СУПЕР