Прежде чем стилизовать кнопки, нужно определиться, какой тег вообще должен стоять на странице — это не вопрос вкуса, а вопрос корректной семантики и доступности.
| Элемент | Когда использовать |
|---|---|
<button> |
Действие происходит на этой же странице: отправка формы, открытие модального окна, переключение состояния (лайк, избранное), любое действие через JavaScript |
<a href="..."> |
Переход на другую страницу или другой раздел сайта — даже если внешне это выглядит как кнопка |
<input type="submit"> |
Устаревающий вариант кнопки отправки формы; в современной вёрстке почти всегда предпочтительнее <button type="submit">, потому что внутрь button можно поместить иконку, вложенный span для текста — то есть куда более гибкую разметку |
Частая ошибка новичков: использовать <div> с обработчиком клика вместо <button>, потому что "так проще стилизовать". Проблема в том, что <div> не получает фокус по Tab, не активируется клавишей Enter/Space, и экранный диктор не объявит его как интерактивный элемент. <button> даёт всё это бесплатно, из коробки — а стилизовать его ничуть не сложнее, если знать, с чего начать (это и есть тема сегодняшнего урока).
<!-- Работает только с мышью, недоступно с клавиатуры и для скринридеров -->
<div class="btn" onclick="addToFavorites()">В избранное</div>
<!-- Кнопка со всей нативной доступностью браузера -->
<button type="button" class="btn" onclick="addToFavorites()">В избранное</button>Мы ещё вернёмся к обработке событий кнопки через JavaScript в модуле про DOM — сегодня фокус только на визуальной стороне.
У <button> есть три значения type, и поведение по умолчанию удивляет очень многих новичков:
type |
Поведение |
|---|---|
submit |
Отправляет форму, внутри которой находится кнопка |
reset |
Сбрасывает все поля формы к исходным значениям |
button |
Не делает ничего сам по себе — просто кнопка для обработки через JavaScript |
Ключевая ловушка: если атрибут type не указан явно, браузер по умолчанию считает кнопку type="submit". Если такая кнопка случайно оказывается внутри <form> (даже если она задумывалась просто как "кнопка меню" или "переключатель"), клик по ней отправит форму — иногда совершенно неожиданно для разработчика.
<form>
<input type="text" name="search" />
<!-- Эта кнопка без явного type отправит форму, хотя задумывалась просто как "очистить" -->
<button onclick="clearField()">Очистить</button>
</form><form>
<input type="text" name="search" />
<!-- Явно указан type="button" — форма не отправится -->
<button type="button" onclick="clearField()">Очистить</button>
</form>Правило: всегда указывайте type у <button> явно, даже если по смыслу это и так submit — это делает код читаемым и предотвращает случайные отправки формы.
Как и input, <button> имеет собственный вид "из коробки" — рамку, фон, шрифт, отступы, которые сильно отличаются между браузерами. Стилизацию всегда начинают со сброса этих особенностей:
<button type="button" class="btn">
Сохранить
</button>
<button type="button" class="btn">
<span>♡</span>
Добавить в избранное
</button>.btn {
/* Сброс браузерных стилей по умолчанию */
border: none;
background: none;
font: inherit; /* подтягивает family, size, line-height от родителя одной строкой */
cursor: pointer;
appearance: none;
/* Собственные стили */
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
transition: background 0.15s ease, transform 0.05s ease;
}Разберём ключевые строки:
font: inherit— сокращённая запись, которая одной строкой подтягиваетfont-family,font-size,line-heightи другие текстовые свойства от родителя. Без неё кнопка часто визуально "не совпадает" по шрифту с остальным текстом страницы — уbuttonв некоторых браузерах свой системный шрифт по умолчанию.cursor: pointer— по умолчанию курсор над кнопкой уже принимает вид "руки" в большинстве браузеров, но это стоит указать явно и не забывать при кастомных интерактивных элементах (например, если кнопка временноdisabled, курсор нужно менять наnot-allowed, разберём ниже).display: inline-flex— удобно, когда внутри кнопки помимо текста есть иконка: flex позволяет ровно выровнять их по центру с помощьюalign-items/justify-content, без лишних танцев сvertical-align.
Кнопка — самый "живой" элемент интерфейса, реагирующий сразу на несколько действий пользователя. Каждое состояние стоит проработать отдельно.
<button type="button" class="btn">Обычная кнопка</button>
<button type="button" class="btn" disabled>Недоступна</button>.btn {
background: #2563eb;
color: white;
}
/* Наведение мышью */
.btn:hover {
background: #1d4ed8;
}
/* Момент нажатия (пока кнопка удерживается) */
.btn:active {
background: #1e40af;
transform: scale(0.98); /* лёгкое "вдавливание" кнопки */
}
/* Фокус с клавиатуры */
.btn:focus-visible {
outline: 2px solid #93c5fd;
outline-offset: 2px;
}
/* Заблокированное состояние */
.btn:disabled {
background: #cbd5e1;
color: #64748b;
cursor: not-allowed;
transform: none;
}Порядок состояний в файле имеет значение. Общепринятый порядок — :hover, затем :active, чтобы состояние "нажато" визуально побеждало состояние "наведено" при одновременном совпадении (когда пользователь наводит мышь и одновременно жмёт кнопку). При использовании :focus-visible для доступности его обычно ставят после :hover, чтобы фокус-индикатор не перекрывался слишком агрессивно, но перед :disabled, которое должно быть финальным и отменять визуальные эффекты предыдущих состояний (transform: none в примере выше — иначе задизейбленная кнопка может "вдавиться", если пользователь всё равно на неё кликнет).
:hover сообщает "курсор сейчас над элементом". :active сообщает нечто более узкое: "элемент прямо сейчас нажат — кнопка мыши/палец ещё не отпущены". Небольшое визуальное "вдавливание" через :active даёт пользователю мгновенную физическую обратную связь о том, что клик действительно зарегистрирован — это особенно заметно и приятно на touch-устройствах.
В реальном интерфейсе редко бывает только один тип кнопки — обычно нужна как минимум "основная" кнопка действия (например, "Сохранить"), "второстепенная" (например, "Отмена") и иногда "опасная" (например, "Удалить"). Хороший подход — общий базовый класс .btn плюс модификаторы:
<button type="submit" class="btn btn--primary">Сохранить</button>
<button type="button" class="btn btn--secondary">Отмена</button>
<button type="button" class="btn btn--danger">Удалить аккаунт</button>.btn {
border: none;
font: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
transition: background 0.15s ease, transform 0.05s ease;
}
/* Основное действие — самое заметное на странице */
.btn--primary {
background: #2563eb;
color: white;
}
.btn--primary:hover {
background: #1d4ed8;
}
/* Второстепенное действие — заметно спокойнее */
.btn--secondary {
background: transparent;
color: #2563eb;
border: 1px solid #2563eb;
}
.btn--secondary:hover {
background: #eff6ff;
}
/* Опасное/разрушающее действие */
.btn--danger {
background: #dc2626;
color: white;
}
.btn--danger:hover {
background: #b91c1c;
}Подход "базовый класс + модификатор" (это, кстати, знакомый паттерн из методологии именования БЭМ — Блок-Элемент-Модификатор) позволяет держать в одном месте всё общее для кнопок (отступы, скругления, шрифт, transition), а различия между вариантами — компактно и явно, без дублирования кода.
<button type="button" class="btn btn--primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<span>Добавить фильм</span>
</button>.btn {
display: inline-flex;
align-items: center;
gap: 8px; /* отступ между иконкой и текстом */
}aria-hidden="true" на <svg> говорит вспомогательным технологиям игнорировать саму иконку — потому что рядом уже есть текстовое описание действия ("Добавить фильм"), и дублировать его озвучиванием графики не нужно.
<button type="button" class="btn btn--icon" aria-label="Добавить в избранное">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 21s-7-4.5-9.5-9C.5 8 2 4 6 4c2 0 3.5 1.5 4 2 0-.5 2-2 4-2 4 0 5.5 4 4.5 8-2.5 4.5-9.5 9-9.5 9z" stroke="currentColor" stroke-width="2" />
</svg>
</button>.btn {
display: inline-flex;
align-items: center;
gap: 8px; /* отступ между иконкой и текстом */
}
.btn--icon {
padding: 8px;
border-radius: 50%;
background: transparent;
color: #374151;
}
.btn--icon:hover {
background: #f3f4f6;
}Если внутри кнопки нет текста вообще — только иконка — aria-label становится обязательным, а не опциональным улучшением. Без него экранный диктор объявит кнопку без какого-либо описания действия, и пользователь, полагающийся на скринридер, просто не поймёт, что эта кнопка делает.
Часто в интерфейсе нужно несколько размеров одной и той же кнопки — например, крупная кнопка на форме и компактная кнопка внутри карточки. Модификаторы размера работают по тому же принципу, что и модификаторы варианта:
<button type="button" class="btn btn--primary btn--sm">Подробнее</button>
<button type="button" class="btn btn--secondary btn--md">Описание</button>
<button type="submit" class="btn btn--primary btn--lg">Оформить заказ</button>.btn--sm {
padding: 6px 12px;
font-size: 13px;
}
.btn--md {
padding: 10px 20px;
font-size: 15px;
}
.btn--lg {
padding: 14px 28px;
font-size: 17px;
}Применим всё вышеописанное к нашему сквозному проекту — странице фильмов.
В практике Урока 11 у нас уже была ссылка "Смотреть" внутри карточки фильма. Оформим её как кнопку визуально, оставаясь семантически ссылкой — ведь по клику пользователь переходит на страницу фильма, а не выполняет действие на этой же странице.
<article class="movie-card">
<img src="https://picsum.photos/300/200?6" alt="Фильм 6" />
<h3>Название фильма 6</h3>
<p>Краткое описание фильма для примера карточки.</p>
<a href="#">Смотреть</a>
</article>В карточках нужно дополнить ссылку классами:
<a href="#" class="btn btn--primary btn--sm">Смотреть</a>и стилизацией к ним:
.btn {
border: none;
font: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-weight: 600;
text-decoration: none; /* важно для <a>, оформленной как кнопка — убираем подчёркивание */
transition: background 0.15s ease, transform 0.05s ease;
}
.btn--primary {
background: #2563eb;
color: white;
}
.btn--primary:hover {
background: #1d4ed8;
}
.btn--primary:focus-visible {
outline: 2px solid #93c5fd;
outline-offset: 2px;
}
.btn--sm {
padding: 6px 14px;
font-size: 14px;
}Важный момент: несмотря на то, что визуально <a> теперь неотличима от <button>, семантика тега осталась прежней — переход по ссылке. Это правильно: браузер по-прежнему покажет предпросмотр адреса при наведении, позволит открыть ссылку в новой вкладке через правый клик, и скринридер объявит её именно как ссылку, а не как кнопку — что соответствует реальному поведению элемента.
Дополним поле поиска из Урока 13 кнопкой отправки в виде иконки лупы:
<form class="search" role="search">
<input
type="search"
class="search__input"
placeholder="Найти фильм..."
aria-label="Поиск по сайту"
/>
<button type="submit" class="btn btn--icon search__submit" aria-label="Найти">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
<path d="M21 21l-4.3-4.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</form>.search {
display: flex;
align-items: center;
gap: 4px;
}
.search__submit {
color: white;
}
.search__submit:hover {
background: rgba(255, 255, 255, 0.15);
}Здесь кнопка сознательно type="submit" — она должна отправлять форму поиска, поэтому явное указание типа не только предотвращает путаницу, но и подтверждает намеренное поведение.
<button type="submit" class="btn btn--secondary btn--sm">Применить фильтры</button>Секретная деталь: это тоже type="submit", потому что фильтры логично оформить как форму (<form> вокруг чекбоксов из Урока 13) — при клике страница получит выбранные значения через параметры запроса. Мы вернёмся к этому подробнее в Уроке 15, когда будем разбирать формы целиком и то, как данные уходят на сервер.
<!-- Опасно: кнопка без type внутри form по умолчанию submit -->
<form>
<input type="text" name="q" />
<button class="btn">Сбросить</button>
</form>Разобрано в разделе 1 — всегда указывайте type явно (button, submit или reset), даже когда поведение по умолчанию случайно совпадает с нужным — явное указание избавляет от догадок при чтении чужого (или своего же, спустя месяц) кода.
<!-- Недоступно с клавиатуры, не объявляется скринридером как интерактивный элемент -->
<div class="btn" onclick="doSomething()">Нажми меня</div>Разобрано в начале урока — используйте <button>, если действие происходит на этой же странице, и не пытайтесь "долепить" доступность к <div> вручную через tabindex и role="button", когда для этого уже существует нативный, полностью доступный тег.
<!-- Скринридер объявит кнопку без какого-либо описания действия -->
<button type="button" class="btn btn--icon">
<svg>...</svg>
</button>Если текста внутри кнопки нет — aria-label не опция "для галочки", а обязательное условие того, чтобы кнопка вообще была понятна пользователям скринридеров.
/* Проблема: у disabled-кнопки остались стили hover/active от активного состояния */
.btn:hover {
background: #1d4ed8;
transform: scale(0.98);
}Если явно не переопределить :disabled в конце (сбросить transform, изменить background, cursor: not-allowed), кнопка может визуально продолжать реагировать так, будто она активна, даже когда взаимодействие с ней заблокировано — это сбивает пользователя с толку.
Убрав браузерный outline через сброс стилей (appearance: none, обнуление border), легко забыть вернуть индикатор фокуса. Без него пользователь, перемещающийся по странице клавишей Tab, не видит, какая именно кнопка сейчас "выбрана" — то есть теряет возможность понять, что произойдёт при нажатии Enter.
Выбор тега — <button> для действий на этой же странице, <a> для переходов — определяет корректное поведение с клавиатуры и для вспомогательных технологий ещё до какой-либо стилизации. У <button> есть скрытая ловушка: без явного type браузер считает его submit, что может привести к неожиданной отправке формы — атрибут type стоит указывать всегда и осознанно.
Стилизация начинается со сброса браузерных стилей по умолчанию (border: none, font: inherit, background: none), а затем последовательно прорабатываются состояния: :hover (наведение), :active (момент нажатия, обычно с лёгким transform: scale()), :focus-visible (индикатор для навигации с клавиатуры) и :disabled (заблокированное состояние, которое должно отменять эффекты остальных состояний).
Практичный паттерн для множества вариантов кнопок — базовый класс .btn с общими свойствами плюс модификаторы (.btn--primary, .btn--secondary, .btn--danger, .btn--sm/.btn--lg), что избавляет от дублирования кода. Кнопка только с иконкой без текста обязательно требует aria-label, иначе она остаётся "немой" для скринридеров.
В следующем уроке мы соберём все элементы форм воедино — input из Урока 13, button из этого урока — и разберём саму структуру <form>: как данные из полей превращаются в запрос, который уходит на сервер.
- Чем принципиально отличается использование
<button>от<a href="...">, оформленной визуально как кнопка? - Что произойдёт, если не указать атрибут
typeу<button>внутри<form>, и почему это может быть проблемой? - Почему для кнопки только с иконкой (без текста) обязателен
aria-label? - Зачем у кнопки прорабатывать отдельно состояния
:hoverи:active, если оба реагируют на взаимодействие мышью? - Почему в состоянии
:disabledважно явно сбрасывать эффекты:hoverи:active, а не полагаться на то, что браузер сделает это сам? - Что делает связка "базовый класс
.btn+ модификатор (.btn--primary,.btn--sm)" удобнее, чем отдельный независимый класс на каждый вариант кнопки?
Создайте три кнопки — .btn--primary, .btn--secondary, .btn--danger — используя базовый класс .btn с модификаторами:
- Общий базовый класс со сбросом браузерных стилей,
border-radius,font-weight,padding,transition. - Для каждого варианта — свои цвета фона/текста и своё состояние
:hover. - Общее для всех вариантов состояние
:focus-visible— видимая обводка для навигации с клавиатуры. - Общее для всех вариантов состояние
:disabled— серый фон, курсорnot-allowed, без эффектов hover/active.
Создайте круглую кнопку-иконку "избранное" (сердечко) для карточки фильма:
- Используйте
<svg>внутри<button>, сaria-hidden="true"на самой иконке иaria-labelна кнопке. - Круглая форма (
border-radius: 50%), прозрачный фон в обычном состоянии. - При
:hover— лёгкий цветной фон. - При
:focus-visible— видимая обводка.
Дополните страницу фильмов (Уроки 11–13):
- Замените текстовые ссылки "Смотреть" в карточках на
<a>, стилизованную как.btn--primary.btn--sm(семантика ссылки сохраняется, но выглядит как кнопка). - Добавьте кнопку-иконку "избранное" из Задания 2 в правый верхний угол каждой карточки.
- В
asideс фильтрами добавьте кнопкуtype="submit""Применить фильтры" с классом.btn--secondary. - В шапке рядом с полем поиска добавьте кнопку отправки в виде иконки лупы (
.btn-icon, но с явнымtype="submit", так как она находится внутри формы поиска). - Проверьте, что каждая кнопка корректно выделяется при переходе по
Tab— нигде не должно быть "невидимого" фокуса.