Каждый раз, когда в проекте появляется небольшая 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. На десктопе атрибут ни на что не влияет.

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. Браузер сам выбирает локализованную надпись или иконку.

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 и начать скроллить страницу, то можно будет увидеть подобное:
Это означает, что запросы появляются по мере скролла.
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". Один атрибут на корне поддерева заменяет всё это.
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 таеого атрибута, который закрывал бы эту задачу.
Комментарии (0)