Кнопка, ссылка, подпись: соберите из div и посчитайте потери

разметка≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Кнопку можно собрать из <div> за минуту: серый фон, рамка, скругления — и на экране её не отличить от настоящей. Разница начинается там, где кнопку нажимают не мышью: с клавиатуры, пальцем на телефоне, программой чтения с экрана, из формы отправкой по Enter. И там подделка ломается сразу в нескольких местах, каждое из которых пришлось бы чинить скриптом. Соберём кнопку, ссылку и подпись из подручных тегов, посчитаем, чего им не хватает, и сравним с настоящими <button>, <a> и <label>.

Разделы темы:·

Возьмём <div>, дадим ему атрибут class="btn" — имя, по которому элемент находят стили, — и покрасим под кнопку. <style> в примере нужен только для вида: правило .btn действует на все элементы с классом btn, точка перед именем и означает класс. Рядом поставим такой же <div> с двумя атрибутами-«починками», настоящую <button> и две ссылки — без адреса и с адресом. Так в одном примере окажется вся коллекция: подделка, подделка с заплатками и два настоящих элемента.

Первая потеря — фокус. Фокус это отметка «клавиатура сейчас здесь»: буквы уходят в поле с фокусом, Enter нажимает кнопку с фокусом, а Tab переносит отметку к следующему элементу. Человек, который не пользуется мышью, без фокуса до кнопки не дотянется вовсе. Проверка по очереди пробует поставить фокус на каждый найденный элемент, и разбор отвечает по каждому: принял или нет. Адрес "div, button, a" через запятую значит «все div, все button и все a», и ответы идут в порядке разметки: пять элементов — пять ответов. Прогноз: где будет «да», а где «нет»?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Простой <div> фокус не принял: для браузера это ящик для раскладки, нажимать в нём нечего. Второй принял — это работа tabindex="0": атрибут ставит элемент в очередь Tab на его место в разметке. <button> и ссылка с адресом получили фокус без всяких атрибутов, а ссылка без адреса — нет.

Атрибут role="button" ни фокуса, ни вида не меняет. Он сообщает программе чтения с экрана, что перед ней кнопка: роль — это то, чем элемент представляется машинам. У многих тегов роль есть и без атрибута: <header>, <nav> и <main> получают её сами, и из таких ролей программа чтения собирает карту ориентиров страницы. Отсюда простая мысль: атрибуты-«починки» описывают элемент чужими словами, а настоящий тег говорит их сам.

Ссылка без адреса — отдельный случай: ей посвящён раздел «Ссылка или кнопка» ниже.

У tabindex три вида значений. 0 — встать в очередь Tab по порядку разметки; именно он нужен самоделке. -1 — Tab элемент пропускает, но фокус на него можно перенести скриптом: так поступают, например, с заголовком, к которому переводят человека после перехода, чтобы «назад» вернул его обратно. Положительные числа выстраивают свою очередь впереди всех остальных, и порядок Tab расходится с тем, что видно на экране, — их не пишут никогда.

focusable проверяет, может ли элемент принять фокус вообще, а не стоит ли он в очереди Tab. На div с tabindex="-1" ответ «да», хотя Tab этот div пропустит.

Язык адресов вроде "div, button, a" и сами стили целиком разбирает вторая глава пролога — начиная с селекторов и каскада. В этой статье непривычные адреса объясняются там, где встречаются, и учить их наперёд не нужно.

Фокус у поддельной кнопки теперь есть. Проверим нажатие — и сразу в условиях, близких к боевым: поставим обе кнопки в форму <form>, из которой данные уходят на сервер. Клавиши проверка нажимать не умеет, зато умеет щёлкать: press щёлкает по первому элементу, который нашёл адрес. Настоящую отправку прибор перехватывает, чтобы пример не ушёл со страницы, и отмечает её в разборе. Прогноз: сколько будет отправок — ноль, одна или две?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Строка одна. Щелчок по <div> до него дошёл, но своего действия у <div> нет — ничего не случилось. Форму отправил <button>.

<button> внутри формы, если не сказать иного, форму отправляет. Это полезно для «Оплатить» и вредно для «Показать пароль» или «Очистить корзину»: такая кнопка тоже отправит форму, и пользователь потеряет введённое. Отсюда правило, которое запоминают на первой же ошибке: у любой кнопки в форме должен быть явный type.

Теперь клавиатура, и здесь проверку придётся заменить руками: нажатие, отправленное из кода, изображает событие, но действия браузера не вызывает. Запустите пример и щёлкните по первой «Купить» — фокус встанет на неё, tabindex работает и для мыши. Нажмите Enter, потом Пробел: не произойдёт ничего, у <div> нет своего действия. Если от Пробела страница уехала вниз, не пугайтесь — Пробел, которому нечего нажимать, листает страницу, а следующий Tab вернёт её к примеру. Теперь Tab: фокус переедет на <button>. Enter отправит форму, и Пробел отправит её же — разбор под примером отметит обе отправки.

На Mac Tab может пропускать кнопки и ссылки. В Safari по ним ходит Option+Tab, а если так же ведёт себя другой браузер, включите навигацию с клавиатуры в системных настройках macOS.

Вот и вторая половина потерь. Чтобы поддельная кнопка нажималась с клавиатуры, её пришлось бы учить скриптом: ловить Enter и Пробел, повторять щелчок, помнить про выключенное состояние, про отправку формы и про то, что Пробел на кнопке не должен листать страницу. Настоящая кнопка приносит весь этот список сама, и забыть что-то из него у неё не получится.

Кнопке, которая действует на странице, но форму отправлять не должна, пишут type="button". Как форма отправляется, какие у кнопки бывают type и как перехватить отправку из скрипта, покажет часть II — статья про отправку формы.

Вторая частая самоделка — ссылка вместо кнопки или кнопка вместо ссылки. Правило короткое: ссылка это переход, кнопка это действие. У ссылки есть адрес, и с ним работают привычные жесты, которых у кнопки нет вовсе: открыть в новой вкладке, скопировать адрес, вернуться кнопкой «назад», добавить в закладки. У кнопки адреса нет, она что-то делает здесь: кладёт товар в корзину, раскрывает меню, отправляет форму.

Раз ссылка держится на адресе, посмотрим на <a> без href. Проверка прочитает не стиль, а свойство самого элемента: tabIndex, с большой I, — это место в очереди Tab, как его хранит браузер, — и спросит, принимает ли элемент фокус. Прогноз для двух строк: какое там число и примет ли такая ссылка фокус?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Число говорит «в очереди», проверка говорит «нет». У <a> свойство tabIndex равно 0 независимо от адреса, а фокусируемой ссылкой браузер считает только <a> с href. Числу из свойства tabIndex верить нельзя — спрашивать надо фокус, и на роль кнопки <a> без href не годится — Tab её не найдёт.

Без адреса <a> — заготовка ссылки, и это не всегда ошибка: так, например, размечают пункт меню текущей страницы, переходить с которого некуда. Число 0 в очереди при этом обманчиво: оно есть у любой ссылки, а вот фокус браузер отдаёт только той, у которой адрес есть.

Ссылка <a href>Кнопка <button>
Что делаетведёт на адресдействует на странице
Срабатывает от клавишEnterEnter и Пробел
Новая вкладка, копия адресаестьнет
Без главного атрибутабез href фокус не принимаетбез type в форме отправляет форму

Клавиши проверяются тем же ручным способом. Вернитесь к первому примеру статьи, запустите его, щёлкните по второй «Купить» — той, что с tabindex, — и дважды нажмите Tab: фокус пройдёт через <button> и встанет на «Корзина», перешагнув ссылку без адреса. Enter отправит вас по адресу /cart: прибор перехватит переход, чтобы пример не ушёл со страницы, и отметит его в разборе. Пробел на той же ссылке не сделает ничего — ссылку он не нажимает, и это второе её отличие от кнопки после адреса.

Кнопка, которая ведёт на другую страницу, и ссылка с href="#", которая раскрывает меню, — одна ошибка с двух сторон. Проверочный вопрос: изменится ли адрес в строке браузера? Да — ссылка, нет — кнопка.

Кнопку «Купить» выключают, пока в заказе ничего не выбрано, — атрибутом disabled. Поставим его обеим кнопкам, настоящей и поддельной, и снова положим их в форму. Прогноз: какие из двух выключенных элементов примут фокус и отправит ли щелчок по выключенной кнопке форму?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Строка одна. Выключенная <button> фокус не взяла, и щелчок по ней форму не отправил. <div> с тем же атрибутом фокус принимает по-прежнему.

Для <div> атрибут disabled — незнакомое слово: браузер хранит его в разметке и ничего с ним не делает. Кнопка остаётся нажимаемой, и «выключенность» существует только в голове того, кто рисовал серый фон.

Когда выключить нужно не одну кнопку, а группу полей, их кладут в <fieldset> — группу с подписью <legend> — и ставят disabled на неё. Проверка ответит числом: сколько элементов нашёл адрес. Двоеточие в адресе :disabled означает не тег, а состояние — «все выключенные элементы». Прогноз: сколько их окажется на странице?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Четыре: отдельная выключенная кнопка, сам <fieldset> и оба элемента внутри — кнопка и поле, хотя своего атрибута у них нет. <div> в счёт не попал: его disabled ничего не значит.

Одним атрибутом на группе замораживают часть формы — например, пока идёт оплата или пока не выбран способ доставки. Без <fieldset> пришлось бы обходить все поля списком и не забыть ни одного, а забытое поле в замороженной форме отправит данные, которых от него не ждали.

У правила «выключено всё внутри» есть исключение: содержимое <legend> группы остаётся включённым. Туда кладут, например, флажок, который включает и выключает всю группу, — сам переключатель пишут скриптом, но достать до флажка человек должен и тогда, когда группа выключена. Прогноз: сколько выключенных насчитает прибор и какое из двух полей примет фокус?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Выключены двое: группа и поле карты. Флажок в <legend> фокус принял — исключение сработало, а поле карты внутри выключенной группы — нет.

Выключенная кнопка пропадает из очереди Tab, и человек с клавиатурой не узнает, почему нажать нельзя. Причину пишут текстом рядом, а не прячут в серой кнопке.

Теперь счёт потерь поддельной кнопки можно свести в одну таблицу. Всё, что в последней колонке, <button> даёт одним тегом.

Что умеет<div><div> с tabindex и role<button>
Принимает фокуснетдада
Называется кнопкой для программы чтениянетдада
Нажимается Enter и Пробеломнетнетда
Понимает disabledнетнетда
Отправляет формунетнетда

Выключенное поле выпадает и из данных формы: его значение не уходит на сервер и не проверяется. Какие поля уходят с формой, разобрано в части II, в статье про сбор данных формы и отправку на сервер, а как браузер сам проверяет поля по атрибутам — в статье про встроенную валидацию.

Последняя самоделка — подпись: слово «Почта» в <span> рядом с полем. Глазу связь видна, браузеру — нет. Связывает подпись с полем <label>, и способов два: атрибут for со значением, равным id поля, или поле прямо внутри <label>. Оба равноправны, вложенный чаще удобнее: не нужно следить за уникальностью id.

Проверим, что даёт связь. Адреса здесь длиннее: квадратные скобки проверяют атрибут, так что label[for] — это label, у которого есть for, label:not([for]) — label без него, а input[type=checkbox] — поля-флажки. Где встанет фокус, видно по рамке на элементе, а отмечен ли флажок — по галочке в квадратике. Прогноз: куда встанет фокус после щелчка по первой подписи и какие флажки окажутся отмечены после щелчков по второй и третьей?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Щелчок по связанной подписи — это щелчок по полю: фокус ушёл в поле почты, флажок «Новости» отметился. <span> «Правила» — просто текст, связи с полем у него нет, и флажок рядом с ним остался пустым.

Человеку с мышью или с телефоном в руке связь даёт большую цель: попасть в квадратик флажка трудно, в слово рядом — легко. Программе чтения она даёт имя поля: поле с <label> объявляется по подписи, а поле рядом со <span> для неё безымянное — она сообщит только, что перед ней поле ввода, и человек не поймёт, что вводить.

Подсказка внутри поля, атрибут placeholder, подпись не заменяет: она исчезает с первой буквой, и человек, отвлёкшийся посреди формы, уже не видит, о чём его здесь спрашивали. А группе вариантов — например, радиокнопкам «Курьером» и «Самовывоз» — нужна ещё и общая подпись: у каждого поля своя есть, а о том, что это за выбор, не говорит ни одна. Её даёт <legend> внутри <fieldset>, знакомого по прошлому разделу.

  • HTML-элемент — это поведение, а не вид: <div>, покрашенный под кнопку, не принимает фокус, не нажимается с клавиатуры, не понимает disabled и не отправляет форму.
  • tabindex="0" ставит элемент в очередь Tab, role="button" называет его кнопкой для программы чтения. Остальное пришлось бы писать скриптом, а <button> даёт всё сразу.
  • Ссылка — переход по адресу, кнопка — действие на странице. Ссылку открывает Enter, кнопку — Enter и Пробел. <a> без href фокус не принимает, хотя tabIndex у неё 0.
  • disabled убирает кнопку из очереди Tab и гасит щелчок, а на <fieldset> выключает всю группу, кроме содержимого <legend>. Выключенные поля не уходят с формой и не проверяются.
  • <label> связывает подпись с полем через for или вложением: щелчок по подписи попадает в поле, программа чтения называет поле по подписи. placeholder подпись не заменяет.
  • Клавиатуру прибор не изображает: Enter и Пробел проверяют руками — Tab по рамке примера и нажатие.

1/4У <div> появились tabindex="0" и role="button". Что он теперь умеет?

2/4Пункт «Условия доставки» открывает страницу /delivery. Какой элемент взять?

3/4У <fieldset disabled> внутри подпись <legend>Оплата</legend>, кнопка и поле. Что выключено?

4/4Чем <span>Почта</span> перед полем хуже <label for="mail">Почта</label>?

Кнопка и ссылка из подручного

На странице магазина две самоделки: «В корзину» из <div> и «Условия доставки» из <span>; второй пункт должен открывать страницу /delivery. Решите, где ссылка, а где кнопка, и перепишите разметку так, чтобы оба элемента принимали фокус, а щелчок по «Условиям доставки» печатал переход.

Классы оставьте: точка в адресе .btn, как и в стилях первого примера статьи, значит «элементы с классом btn», и по классам проверка найдёт элементы, какой бы тег вы ни поставили.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

«В корзину» ничего не открывает, это действие — значит, <button>. «Условия доставки» меняют адрес — значит, ссылка, и обязательно с href: без адреса она не примет фокус и не напечатает переход.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод: .btn, .link → фокус: да, да и переход: /delivery. role и tabindex больше не нужны: настоящие элементы приносят фокус и роль сами.

Подписи к форме доставки

В форме доставки подписи набраны <span>: щелчок по «Самовывоз» не выбирает вариант, щелчок по «Комментарий» не ставит курсор в поле, а у группы вариантов нет общей подписи. Свяжите каждую подпись с её полем и дайте группе подпись <legend> вместо первого <span>.

Подписям оставьте их id: решётка в адресе #t-pickup значит «элемент с id t-pickup», и проверка щёлкает именно по ним. input[type=radio] в последней строке — радиокнопки: тот же адрес по атрибуту, что у флажков в разделе о подписях.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Радиокнопку удобно положить внутрь <label>, поле комментария — связать через for: оба способа равноправны. Первый <span> группы становится <legend>.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод: фокус: input#note и input[type=radio] → checked: false, true. Щелчок по «Самовывоз» выбрал вариант, щелчок по «Комментарий» перенёс фокус в поле.