Каждый раз, когда в проекте появляется небольшая UI-задача — сделать поле для email с правильной мобильной клавиатурой, лениво подгрузить картинки, заблокировать фон под модалкой, разрешить пользователю отредактировать кусок текста прямо на странице, — первая реакция многих джунов: «надо подключить библиотеку» или «надо повесить обработчик». Часто всего этого можно избежать одним атрибутом в HTML.

Ниже — подборка таких атрибутов, сгруппированных не по алфавиту, а по задаче, которую они закрывают.

Мобильная клавиатура и подсказки в формах

Половина проблем с формами на телефоне решается тем, что разработчик не задал тип клавиатуры. Браузер не догадается сам — ему нужно сказать.

inputmode — какую клавиатуру показать

inputmode в отличие от type не валидирует значение и не меняет поведение поля, а только подсказывает мобильному браузеру, какую раскладку открыть. Полезно, когда type="text" по другим причинам менять нельзя.

<!-- цифровая клавиатура без минуса и точки -->
<input type="text" inputmode="numeric" name="pin">

<!-- клавиатура с точкой/запятой для дробных -->
<input type="text" inputmode="decimal" name="price">

<!-- клавиатура с @ и .com -->
<input type="text" inputmode="email" name="email">

<!-- клавиатура с / и .com -->
<input type="text" inputmode="url" name="site">

Доступные значения: none, text, numeric, decimal, tel, email, url, search. На десктопе атрибут ни на что не влияет.

Сравнение мобильной клавиатуры с разными значениями атрибута inputmode

Поддержка браузерами
chrome
Chrome
66
firefox
Firefox
95
edge
Edge
79
safari
Safari
12.1
opera
Opera
53

enterkeyhint — надпись на клавише Enter

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

<input type="search" enterkeyhint="search">
<input type="text" enterkeyhint="done">
<textarea enterkeyhint="send"></textarea>

Значения: enter, done, go, next, previous, search, send. Браузер сам выбирает локализованную надпись или иконку.

Сравнение мобильной клавиатуры с разными значениями атрибута enterkeyhint

Поддержка браузерами
chrome
Chrome
77
firefox
Firefox
94
edge
Edge
79
safari
Safari
13.1
opera
Opera
66

autocomplete — что предлагать в подсказках

Браузер по умолчанию пытается подсказывать значения для полей формы из истории. Поведение можно либо выключить, либо — что важнее — уточнить, чтобы менеджер паролей и автозаполнение Chrome/Safari знали, какое именно поле перед ними.

<!-- логин-форма: пара полей, которую браузер опознает как логин/пароль -->
<input name="user" autocomplete="username">
<input name="pass" type="password" autocomplete="current-password">

<!-- регистрация: пароль нужно сгенерировать, не подставлять старый -->
<input name="pass" type="password" autocomplete="new-password">

<!-- адрес доставки: каждое поле размечено отдельно -->
<input name="zip" autocomplete="postal-code">
<input name="country" autocomplete="country-name">

<!-- одноразовый код из SMS -->
<input name="otp" autocomplete="one-time-code" inputmode="numeric">

<!-- выключить автодополнение совсем -->
<input name="captcha" autocomplete="off">

Значений у autocomplete около пятидесяти — покрывают всё от given-name до cc-csc (CVV банковской карты). Конкретный список лучше посмотреть на странице атрибута в MDN. Чем точнее размечена форма, тем меньше пользователь печатает руками — и тем выше конверсия.

accept — фильтр форматов для загрузки файлов

Когда у <input type="file"> не указан accept, пользователю показываются вообще все файлы — и он спокойно загружает архив вместо аватарки. Атрибут принимает либо MIME-типы, либо расширения через запятую.

<!-- только изображения, любой формат -->
<input type="file" accept="image/*">

<!-- только конкретные расширения -->
<input type="file" accept=".jpg,.jpeg,.png,.webp">

<!-- сразу с камеры на мобильном -->
<input type="file" accept="image/*" capture="environment">

<!-- PDF и DOCX -->
<input type="file" accept="application/pdf,.docx">

Важная оговорка: accept — это удобство, а не валидация. Файл с расширением .png, у которого внутри JPEG или, что хуже, исполняемое содержимое, через фильтр пройдёт. Проверка типа на бэкенде по реальным байтам файла обязательна, что бы ни стояло в accept.

Управление загрузкой ресурсов

Часть атрибутов влияет на то, в какой момент и в каком порядке браузер тянет ресурсы со страницы. Без них приходится либо терпеть медленную загрузку, либо вешать ленивую подгрузку через IntersectionObserver.

loading="lazy" — ленивые картинки и iframe из коробки

Браузер сам отложит загрузку изображения или встроенного фрейма до того момента, пока пользователь к нему не доскроллит. Никакого JS не нужно.

<img src="cover.jpg" loading="lazy" width="1200" height="700">
<iframe src="https://www.youtube.com/embed/..." loading="lazy"></iframe>

Два важных момента. Первый: width и height у картинки обязательны — без них браузер не зарезервирует место, и при подгрузке страница «дёрнется» (CLS). Второй: для картинки, которая находится выше первого экрана (LCP), loading="lazy" ставить вредно — она замедлит первый рендер вместо того, чтобы ускорить.

Если на сайте с картинками, имеющими loading="lazy" зайти в панель Network в DevTools и начать скроллить страницу, то можно будет увидеть подобное:

Это означает, что запросы появляются по мере скролла.

Поддержка браузерами
chrome
Chrome
77
firefox
Firefox
121
edge
Edge
79
safari
Safari
16.4
opera
Opera
64

defer и async — когда выполнять скрипт

Оба атрибута относятся к <script src="..."> с внешним файлом (на inline-скриптах не работают) и решают одну задачу: не заставлять браузер останавливать парсинг HTML, чтобы скачать и выполнить JS.

<!-- блокирующий: качается и выполняется прямо в этой точке HTML -->
<script src="/js/analytics.js"></script>

<!-- defer: качается параллельно с парсингом, выполняется ПОСЛЕ DOMContentLoaded,
     порядок выполнения сохраняется -->
<script defer src="/js/app.js"></script>
<script defer src="/js/widget.js"></script>

<!-- async: качается параллельно, выполняется как только скачался,
     порядок НЕ гарантирован -->
<script async src="/js/counter.js"></script>

Практическое правило. Если скрипт — часть приложения и зависит от DOM или от других скриптов — defer. Если это независимая аналитика / счётчик, который ни от чего не зависит и сам ни от кого не зависит, — async. Скрипты с type="module" по умолчанию ведут себя как defer, отдельный атрибут им не нужен.

Видимость, фокус и клавиатурная навигация

Глобальные атрибуты, которые управляют тем, видит ли элемент пользователь, дотягивается ли до него клавиатурой и попадает ли он в дерево доступности.

hidden — простое скрытие без CSS

Аналог display: none: элемент с hidden не отображается, не получает место в потоке и не доступен ассистивным технологиям. Удобно, когда скрытие — начальное состояние, и не хочется ради этого добавлять класс или inline-стиль.

<!-- кнопка для авторизованных пользователей -->
<button hidden id="logout">Выйти</button>
// показать через JS — достаточно убрать атрибут
document.getElementById('logout').hidden = false;

В современных браузерах поддерживается значение hidden="until-found" — элемент скрыт, но текст внутри индексируется поиском по странице (Ctrl+F) и встроенным переводчиком; при обнаружении совпадения браузер автоматически раскрывает блок.

inert — заморозить поддерево

Атрибут пары к hidden, но решает другую задачу. inert оставляет элемент видимым, но делает всё его поддерево неинтерактивным: клики не срабатывают, фокус через Tab проскакивает мимо, скринридер игнорирует. Главный сценарий — модальные окна.

<!-- пока модалка открыта, остальная страница inert -->
<main inert>
  ... весь основной контент ...
</main>
<dialog open>
  <p>Подтвердите удаление</p>
  <button>Да</button>
  <button>Отмена</button>
</dialog>
// открыть модалку
main.inert = true;
dialog.showModal();

// закрыть
dialog.close();
main.inert = false;

До inert ту же логику собирали вручную: рекурсивно вешали tabindex="-1", ловили клики на оверлее, ставили aria-hidden="true". Один атрибут на корне поддерева заменяет всё это.

Поддержка браузерами
chrome
Chrome
102
firefox
Firefox
112
edge
Edge
102
safari
Safari
15.5
opera
Opera
88

tabindex — порядок и доступность для клавиатуры

Атрибут с тремя осмысленными режимами:

  • tabindex="0" — сделать элемент, который по умолчанию не фокусируется (div, span, custom-блок), частью Tab-цепочки в естественном порядке документа.
  • tabindex="-1" — элемент можно сфокусировать программно через element.focus(), но Tab-ом до него не дойти. Используется, например, чтобы после открытия модалки переставить фокус внутрь.
  • Любое положительное число — задаёт явный порядок обхода: сначала все элементы с tabindex="1", потом с "2" и так далее, и только после — всё остальное. Звучит удобно, на практике — почти всегда антипаттерн: ломает естественный порядок документа и сбивает скринридеры. В живом коде встречается крайне редко.
<!-- кастомная кнопка из div - попадает в Tab -->
<div tabindex="0" role="button">Сохранить</div>

<!-- модальный заголовок - фокусируется только через JS -->
<h2 tabindex="-1" id="dialog-title">Подтверждение</h2>

autofocus — фокус сразу после загрузки

Глобальный атрибут (раньше работал только на полях формы, теперь — на любом фокусируемом элементе): после загрузки страницы или открытия диалога браузер сам поставит фокус на этот элемент.

<input name="search" autofocus>
<dialog open>
  <input name="confirm" autofocus>
</dialog>

На одной странице должен быть только один элемент с autofocus. Если их несколько, браузер выберет первый и проигнорирует остальные. И ещё — не злоупотреблять: автофокус на странице, куда пользователь только что зашёл и пытается прочитать заголовок, сбивает с толку и мешает скринридеру.

Интерактивный контент прямо в HTML

Группа атрибутов, которые превращают обычный элемент в интерактивный без подключения библиотек.

contenteditable — редактируемая область

Сделать редактируемым можно практически любой блочный элемент. Это не замена textarea — здесь можно редактировать HTML с разметкой, картинками, вложенными блоками. Так устроены простые WYSIWYG-редакторы и многие inline-правки прямо в карточке.

<div contenteditable="true">
  <p>Этот текст редактируется <b>прямо</b> на странице.</p>
</div>

С 2023 года поддерживается значение contenteditable="plaintext-only" — редактируется только текст, без вставки разметки из буфера обмена. Удобно для полей-комментариев, куда не нужно тащить из Word жирный шрифт и цветной фон.

spellcheck — проверка орфографии браузером

По умолчанию браузер проверяет орфографию в textarea, в обычных input type="text" и в редактируемых через contenteditable блоках. Атрибут позволяет это явно включить или выключить.

<!-- выключить проверку: код, ники, ключи API -->
<textarea spellcheck="false">const userId = abcd1234;</textarea>

<!-- принудительно включить проверку в редактируемом блоке -->
<div contenteditable="true" spellcheck="true">...</div>

Часто забывают выключить spellcheck в полях для логина или поля для ввода кода — и тогда у пользователя по никнейму ползёт красная волнистая линия, что выглядит как ошибка валидации.

draggable — drag-and-drop без библиотек

Превращает элемент в перетаскиваемый. Сам по себе атрибут только «разрешает» перетаскивание — что с ним делать, описывают JS-обработчики dragstart, dragover, drop.

<div draggable="true" data-id="42">Перетащи меня</div>
<div class="dropzone">Зона</div>
card.addEventListener('dragstart', e => {
  e.dataTransfer.setData('text/plain', card.dataset.id);
});
zone.addEventListener('dragover', e => e.preventDefault());
zone.addEventListener('drop', e => {
  const id = e.dataTransfer.getData('text/plain');
  console.log('бросили карточку', id);
});

Картинки и ссылки draggable="true" по умолчанию — для простого drag достаточно подписаться на события без атрибута. Для всего остального (div, li, кастомных карточек) атрибут обязателен.

Безопасность, локализация и метаданные

Последняя группа — атрибуты, у которых пересекаются темы безопасности, локализации и работы с данными.

sandbox — ограничения внутри iframe

Внутри обычного <iframe> чужая страница может делать почти всё: запускать скрипты, отправлять формы, открывать всплывающие окна, ставить куки. Если встраивается контент, которому не до конца доверяешь, — стороннее видео, виджет, превью пользовательской ссылки — sandbox позволяет это запретить.

<!-- максимально ограничено: ни JS, ни форм, ни куки -->
<iframe src="preview.html" sandbox></iframe>

<!-- разрешить JS и собственное хранилище, но запретить всё остальное -->
<iframe src="widget.html" sandbox="allow-scripts allow-same-origin"></iframe>

Пустой sandbox — самый строгий режим. Дальше через пробел добавляются разрешения: allow-scripts, allow-forms, allow-same-origin, allow-popups, allow-modals и другие. Включают только те, без которых вложенная страница не работает.

Тонкость: одновременно ставить allow-scripts и allow-same-origin для контента с того же домена — означает фактически выключить песочницу: скрипт изнутри сможет программно удалить атрибут у iframe-родителя. Для пользовательского контента такая комбинация — антипаттерн.

download — скачать вместо перехода

На обычной ссылке браузер будет скачативаь файл, а не открывать его. Заодно можно задать имя, под которым файл сохранится.

<!-- скачать как есть -->
<a href="/files/report-2024.pdf" download>Отчёт</a>

<!-- скачать под другим именем -->
<a href="/files/report-2024.pdf" download="otchet-itogi.pdf">Отчёт</a>

Работает только для ссылок на тот же origin, что и страница — для кросс-доменных URL браузер атрибут проигнорирует и просто откроет файл. Плюс к этому атрибут хорошо сочетается с data-URL и Blob: можно собрать файл прямо в JS и предложить пользователю скачать его без обращения к серверу.

translate — не переводить этот блок

Атрибут говорит сервисам автоперевода (Google Translate, встроенный переводчик Chrome/Safari) не трогать содержимое элемента. Применяется ко всему, что в любом случае должно остаться нетронутым: торговые марки, имена пользователей, технические идентификаторы, фрагменты кода в тексте.

<p>Откройте файл <span translate="no">package.json</span> и найдите <span translate="no">devDependencies</span>.</p>

Без атрибута переводчик может превратить package.json в что-нибудь вроде «упаковка.json» и сломать инструкцию.

data-* — собственные атрибуты с данными

Стандартный способ повесить на элемент любые свои данные, не нарушая валидность HTML. Имя должно начинаться с data- и быть в нижнем регистре; в JS доступ — через свойство dataset, где имена переписаны в camelCase.

<button data-action="delete" data-item-id="42" data-confirm-text="Точно удалить?">
  Удалить
</button>
document.querySelectorAll('[data-action]').forEach(btn => {
  btn.addEventListener('click', () => {
    const { action, itemId, confirmText } = btn.dataset;
    if (confirm(confirmText)) doAction(action, itemId);
  });
});

Что важно понимать: data-* — это атрибут DOM, а не свойство JS-объекта. При записи большого объекта браузер вызовет у него toString() и в HTML окажется строка [object Object]. Сериализовать в JSON и парсить обратно нужно вручную — и хранить там стоит только метаданные, а не реальное состояние приложения.

Что из этого даёт самый большой эффект

Если выбирать три атрибута, которые точно стоит расставить по проекту в первую неделю работы, — это loading="lazy" на всех картинках ниже первого экрана, autocomplete на формах с логином/адресом/платежами и inert на фоне модальных окон. Они без библиотек закрывают типичную просадку по производительности, конверсии и доступности.

Остальные — ситуативные. inputmode и enterkeyhint заметно меняют ощущение от формы на мобильном. contenteditable и draggable — полезны точечно, когда нужно небольшое UI-взаимодействие, не оправдывающее подключение библиотеки. sandbox — обязателен для пользовательского контента в iframe. Каждый раз, прежде чем писать обработчик или подключать пакет, стоит проверить: нет ли в HTML таеого атрибута, который закрывал бы эту задачу.