Skip to content

Latest commit

 

History

History
514 lines (385 loc) · 29.1 KB

File metadata and controls

514 lines (385 loc) · 29.1 KB

Урок 14. Стилизация button

button vs a vs input type="submit" — когда что использовать

Прежде чем стилизовать кнопки, нужно определиться, какой тег вообще должен стоять на странице — это не вопрос вкуса, а вопрос корректной семантики и доступности.

Элемент Когда использовать
<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 — сегодня фокус только на визуальной стороне.


Атрибут type у button — скрытая ловушка

У <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.

Состояния кнопки: :hover, :active, :focus-visible, :disabled

Кнопка — самый "живой" элемент интерфейса, реагирующий сразу на несколько действий пользователя. Каждое состояние стоит проработать отдельно.

<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 в примере выше — иначе задизейбленная кнопка может "вдавиться", если пользователь всё равно на неё кликнет).

Почему :active полезен даже с :hover

:hover сообщает "курсор сейчас над элементом". :active сообщает нечто более узкое: "элемент прямо сейчас нажат — кнопка мыши/палец ещё не отпущены". Небольшое визуальное "вдавливание" через :active даёт пользователю мгновенную физическую обратную связь о том, что клик действительно зарегистрирован — это особенно заметно и приятно на touch-устройствах.


Варианты кнопок: primary / secondary / danger

В реальном интерфейсе редко бывает только один тип кнопки — обычно нужна как минимум "основная" кнопка действия (например, "Сохранить"), "второстепенная" (например, "Отмена") и иногда "опасная" (например, "Удалить"). Хороший подход — общий базовый класс .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> говорит вспомогательным технологиям игнорировать саму иконку — потому что рядом уже есть текстовое описание действия ("Добавить фильм"), и дублировать его озвучиванием графики не нужно.

Кнопка только с иконкой — обязателен aria-label

<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;
}

Практика: кнопки на странице фильмов

Применим всё вышеописанное к нашему сквозному проекту — странице фильмов.

Кнопка "Смотреть" в карточке (сейчас — <a>, оформленная как кнопка)

В практике Урока 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>, семантика тега осталась прежней — переход по ссылке. Это правильно: браузер по-прежнему покажет предпросмотр адреса при наведении, позволит открыть ссылку в новой вкладке через правый клик, и скринридер объявит её именно как ссылку, а не как кнопку — что соответствует реальному поведению элемента.

Кнопка отправки поиска в шапке (иконка-кнопка внутри <form>)

Дополним поле поиска из Урока 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 у кнопки внутри формы

<!-- Опасно: кнопка без type внутри form по умолчанию submit -->
<form>
  <input type="text" name="q" />
  <button class="btn">Сбросить</button>
</form>

Разобрано в разделе 1 — всегда указывайте type явно (button, submit или reset), даже когда поведение по умолчанию случайно совпадает с нужным — явное указание избавляет от догадок при чтении чужого (или своего же, спустя месяц) кода.

<div> вместо <button>

<!-- Недоступно с клавиатуры, не объявляется скринридером как интерактивный элемент -->
<div class="btn" onclick="doSomething()">Нажми меня</div>

Разобрано в начале урока — используйте <button>, если действие происходит на этой же странице, и не пытайтесь "долепить" доступность к <div> вручную через tabindex и role="button", когда для этого уже существует нативный, полностью доступный тег.

Кнопка без aria-label, если внутри только иконка

<!-- Скринридер объявит кнопку без какого-либо описания действия -->
<button type="button" class="btn btn--icon">
  <svg>...</svg>
</button>

Если текста внутри кнопки нет — aria-label не опция "для галочки", а обязательное условие того, чтобы кнопка вообще была понятна пользователям скринридеров.

Забытое состояние :disabled

/* Проблема: у disabled-кнопки остались стили hover/active от активного состояния */
.btn:hover {
  background: #1d4ed8;
  transform: scale(0.98);
}

Если явно не переопределить :disabled в конце (сбросить transform, изменить background, cursor: not-allowed), кнопка может визуально продолжать реагировать так, будто она активна, даже когда взаимодействие с ней заблокировано — это сбивает пользователя с толку.

Отсутствие :focus-visible при кастомной кнопке

Убрав браузерный 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>: как данные из полей превращаются в запрос, который уходит на сервер.


Вопросы для проверки

  1. Чем принципиально отличается использование <button> от <a href="...">, оформленной визуально как кнопка?
  2. Что произойдёт, если не указать атрибут type у <button> внутри <form>, и почему это может быть проблемой?
  3. Почему для кнопки только с иконкой (без текста) обязателен aria-label?
  4. Зачем у кнопки прорабатывать отдельно состояния :hover и :active, если оба реагируют на взаимодействие мышью?
  5. Почему в состоянии :disabled важно явно сбрасывать эффекты :hover и :active, а не полагаться на то, что браузер сделает это сам?
  6. Что делает связка "базовый класс .btn + модификатор (.btn--primary, .btn--sm)" удобнее, чем отдельный независимый класс на каждый вариант кнопки?

Практические задания

Задание 1. Три варианта кнопок с полным набором состояний

Создайте три кнопки — .btn--primary, .btn--secondary, .btn--danger — используя базовый класс .btn с модификаторами:

  1. Общий базовый класс со сбросом браузерных стилей, border-radius, font-weight, padding, transition.
  2. Для каждого варианта — свои цвета фона/текста и своё состояние :hover.
  3. Общее для всех вариантов состояние :focus-visible — видимая обводка для навигации с клавиатуры.
  4. Общее для всех вариантов состояние :disabled — серый фон, курсор not-allowed, без эффектов hover/active.

Задание 2. Кнопка только с иконкой

Создайте круглую кнопку-иконку "избранное" (сердечко) для карточки фильма:

  1. Используйте <svg> внутри <button>, с aria-hidden="true" на самой иконке и aria-label на кнопке.
  2. Круглая форма (border-radius: 50%), прозрачный фон в обычном состоянии.
  3. При :hover — лёгкий цветной фон.
  4. При :focus-visible — видимая обводка.

Задание 3. Объединяющая практика — кнопки на странице фильмов

Дополните страницу фильмов (Уроки 11–13):

  1. Замените текстовые ссылки "Смотреть" в карточках на <a>, стилизованную как .btn--primary.btn--sm (семантика ссылки сохраняется, но выглядит как кнопка).
  2. Добавьте кнопку-иконку "избранное" из Задания 2 в правый верхний угол каждой карточки.
  3. В aside с фильтрами добавьте кнопку type="submit" "Применить фильтры" с классом .btn--secondary.
  4. В шапке рядом с полем поиска добавьте кнопку отправки в виде иконки лупы (.btn-icon, но с явным type="submit", так как она находится внутри формы поиска).
  5. Проверьте, что каждая кнопка корректно выделяется при переходе по Tab — нигде не должно быть "невидимого" фокуса.

Предыдущий урок | Следующий урок