Кнопка, ссылка, подпись: соберите из div и посчитайте потери
Здесь будет встроен видео-разбор темы (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», и ответы идут в порядке разметки: пять элементов — пять ответов. Прогноз: где будет «да», а где «нет»?
<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 щёлкает по первому элементу, который нашёл адрес. Настоящую отправку прибор перехватывает, чтобы пример не ушёл со страницы, и отмечает её в разборе. Прогноз: сколько будет отправок — ноль, одна или две?
<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, как его хранит браузер, — и спросит, принимает ли элемент фокус. Прогноз для двух строк: какое там число и примет ли такая ссылка фокус?
<a> свойство tabIndex равно 0 независимо от адреса, а фокусируемой ссылкой браузер считает только <a> с href. Числу из свойства tabIndex верить нельзя — спрашивать надо фокус, и на роль кнопки <a> без href не годится — Tab её не найдёт.Без адреса <a> — заготовка ссылки, и это не всегда ошибка: так, например, размечают пункт меню текущей страницы, переходить с которого некуда. Число 0 в очереди при этом обманчиво: оно есть у любой ссылки, а вот фокус браузер отдаёт только той, у которой адрес есть.
Ссылка <a href> | Кнопка <button> | |
|---|---|---|
| Что делает | ведёт на адрес | действует на странице |
| Срабатывает от клавиш | Enter | Enter и Пробел |
| Новая вкладка, копия адреса | есть | нет |
| Без главного атрибута | без href фокус не принимает | без type в форме отправляет форму |
Клавиши проверяются тем же ручным способом. Вернитесь к первому примеру статьи, запустите его, щёлкните по второй «Купить» — той, что с tabindex, — и дважды нажмите Tab: фокус пройдёт через <button> и встанет на «Корзина», перешагнув ссылку без адреса. Enter отправит вас по адресу /cart: прибор перехватит переход, чтобы пример не ушёл со страницы, и отметит его в разборе. Пробел на той же ссылке не сделает ничего — ссылку он не нажимает, и это второе её отличие от кнопки после адреса.
Кнопка, которая ведёт на другую страницу, и ссылка с href="#", которая раскрывает меню, — одна ошибка с двух сторон. Проверочный вопрос: изменится ли адрес в строке браузера? Да — ссылка, нет — кнопка.
Кнопку «Купить» выключают, пока в заказе ничего не выбрано, — атрибутом disabled. Поставим его обеим кнопкам, настоящей и поддельной, и снова положим их в форму. Прогноз: какие из двух выключенных элементов примут фокус и отправит ли щелчок по выключенной кнопке форму?
<button> фокус не взяла, и щелчок по ней форму не отправил. <div> с тем же атрибутом фокус принимает по-прежнему.Для <div> атрибут disabled — незнакомое слово: браузер хранит его в разметке и ничего с ним не делает. Кнопка остаётся нажимаемой, и «выключенность» существует только в голове того, кто рисовал серый фон.
Когда выключить нужно не одну кнопку, а группу полей, их кладут в <fieldset> — группу с подписью <legend> — и ставят disabled на неё. Проверка ответит числом: сколько элементов нашёл адрес. Двоеточие в адресе :disabled означает не тег, а состояние — «все выключенные элементы». Прогноз: сколько их окажется на странице?
<fieldset> и оба элемента внутри — кнопка и поле, хотя своего атрибута у них нет. <div> в счёт не попал: его disabled ничего не значит.Одним атрибутом на группе замораживают часть формы — например, пока идёт оплата или пока не выбран способ доставки. Без <fieldset> пришлось бы обходить все поля списком и не забыть ни одного, а забытое поле в замороженной форме отправит данные, которых от него не ждали.
У правила «выключено всё внутри» есть исключение: содержимое <legend> группы остаётся включённым. Туда кладут, например, флажок, который включает и выключает всю группу, — сам переключатель пишут скриптом, но достать до флажка человек должен и тогда, когда группа выключена. Прогноз: сколько выключенных насчитает прибор и какое из двух полей примет фокус?
<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] — поля-флажки. Где встанет фокус, видно по рамке на элементе, а отмечен ли флажок — по галочке в квадратике. Прогноз: куда встанет фокус после щелчка по первой подписи и какие флажки окажутся отмечены после щелчков по второй и третьей?
<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», и по классам проверка найдёт элементы, какой бы тег вы ни поставили.
«В корзину» ничего не открывает, это действие — значит, <button>. «Условия доставки» меняют адрес — значит, ссылка, и обязательно с href: без адреса она не примет фокус и не напечатает переход.
Вывод: .btn, .link → фокус: да, да и переход: /delivery. role и tabindex больше не нужны: настоящие элементы приносят фокус и роль сами.
Подписи к форме доставки
В форме доставки подписи набраны <span>: щелчок по «Самовывоз» не выбирает вариант, щелчок по «Комментарий» не ставит курсор в поле, а у группы вариантов нет общей подписи. Свяжите каждую подпись с её полем и дайте группе подпись <legend> вместо первого <span>.
Подписям оставьте их id: решётка в адресе #t-pickup значит «элемент с id t-pickup», и проверка щёлкает именно по ним. input[type=radio] в последней строке — радиокнопки: тот же адрес по атрибуту, что у флажков в разделе о подписях.
Радиокнопку удобно положить внутрь <label>, поле комментария — связать через for: оба способа равноправны. Первый <span> группы становится <legend>.
Вывод: фокус: input#note и input[type=radio] → checked: false, true. Щелчок по «Самовывоз» выбрал вариант, щелчок по «Комментарий» перенёс фокус в поле.