Baseline: апрель 2026
- суббота, 2 мая 2026 г. в 00:00:02
Обзор на браузерные API, которые стали Widely available в апреле 2026. Раз в месяц я буду вам напоминать, что вы уже можете использовать в проде.
Каждый месяц выходят новые CSS-свойства, HTML-атрибуты, JavaScript-методы и WebAPI, но применять в проде мы их конечно же не будем.
2.5 года назад также каждый месяц выходили новые фичи в браузере, а вот их уже пора начинать применять.
У каждой компании, да что уж там компании, у каждой команды в компании своя методика принятия решения о внедрении той или иной фичи в проекте.
Общий же сценарий выглядит так:
- Посмотрели в пользовательские метрики. Поняли какими браузерами и их версиями в основном пользуются пользователи проекта;
- Заглянули в caniuse и поняли, какие фичи уже поддерживаются большинством браузеров;
- Приняли решение о внедрении той или иной фичи в проект.
Какие-то команды позволяют себе указывать правило "последние три версии браузеров". У других специфика проекта, что проект работает исключительно на iPad с Safari. Сами понимаете, все мы разные и требования разные, и у каждого свой подход.
Baseline - позволяет немного упростить процесс принятия решения о внедрении той или иной фичи в проект. Если фича Widely available значит фича уже как минимум есть во всех основных браузерах как минимум стабильно используются последние 2.5 года.
<search>
Web authentication easy public key access
String isWellFormed() and toWellFormed()
ARIA attribute reflection
<search> — это HTML-элемент, который обозначает часть страницы или приложения, связанную с поиском или фильтрацией. Внутри него могут лежать форма поиска, поле ввода, чекбоксы фильтров, быстрые подсказки или другие элементы интерфейса, которые помогают пользователю что-то найти.
Важно: <search> не выводит результаты поиска сам по себе. Это не аналог <ul> или <section> для результатов. Он нужен именно для области управления поиском: поля ввода, фильтров, кнопок, быстрых подсказок. Сами результаты обычно остаются в основном содержимом страницы.
С точки зрения доступности <search> автоматически создаёт недостающий landmark search, поэтому:
<form role="search"> ... </form>
теперь можно заменить на:
<search> <form> ... </form> </search>
У элемента нет специальных атрибутов — только глобальные HTML-атрибуты вроде class, id, title, aria-label, hidden, data-* и других.
Базовый вариант:
<search> <!-- элементы поиска или фильтрации --> </search>
Чаще всего внутри будет форма:
<search> <form action="/search/"> <label for="site-search">Поиск по сайту</label> <input type="search" id="site-search" name="q"> <button type="submit">Найти</button> </form> </search>
Ещё один полезный момент: на странице может быть несколько областей поиска. Например, глобальный поиск по сайту в шапке и отдельный фильтр внутри каталога. В таком случае им можно дать понятные имена через aria-label или title.
<header> <search aria-label="Поиск по сайту"> ... </search> </header> <main> <search aria-label="Фильтр товаров"> ... </search> </main>
<search> не делает поиск «рабочим» и не заменяет JavaScript или серверную логику. Он просто честно описывает смысл участка интерфейса: вот здесь пользователь ищет или фильтрует данные. Благодаря этому HTML становится понятнее и для разработчиков, и для браузеров, и для вспомогательных технологий.
Источник: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/search
Web authentication easy public key access — это небольшое, но полезное улучшение WebAuthn: браузер даёт удобные методы, чтобы достать данные нового публичного ключа из AuthenticatorAttestationResponse, не разбирая вручную бинарный attestationObject. Речь про методы getAuthenticatorData(), getPublicKey() и getPublicKeyAlgorithm().
Эти данные появляются во время регистрации passkey / WebAuthn-ключа. Пользователь создаёт новый ключ через navigator.credentials.create(), браузер возвращает PublicKeyCredential, а внутри его response лежит объект AuthenticatorAttestationResponse. Он содержит информацию, которая нужна серверу, чтобы потом проверять вход пользователя: идентификатор credential, публичный ключ и алгоритм.
Главная польза фичи — меньше ручного парсинга. Раньше разработчику приходилось доставать публичный ключ из attestationObject, а теперь можно вызвать готовый метод:
const publicKey = credential.response.getPublicKey();
Методы вызываются у объекта AuthenticatorAttestationResponse, который можно получить после успешного вызова navigator.credentials.create():
const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions, }); const response = credential.response;
Дальше доступны методы:
response.getAuthenticatorData(); response.getPublicKey(); response.getPublicKeyAlgorithm();
getAuthenticatorData() возвращает ArrayBuffer с authenticator data из attestationObject. Это данные от аутентификатора: например, флаги, счётчик подписи и информация о созданном credential.
const authenticatorData = response.getAuthenticatorData();
getPublicKey() возвращает ArrayBuffer с публичным ключом нового credential в формате DER SubjectPublicKeyInfo. Если публичный ключ недоступен, метод вернёт null. Этот ключ нужно сохранить на сервере, чтобы потом проверять операции входа через navigator.credentials.get().
const publicKey = response.getPublicKey(); if (publicKey === null) { throw new Error('Публичный ключ недоступен'); }
getPublicKeyAlgorithm() возвращает число — идентификатор криптографического алгоритма COSE. Эту информацию тоже нужно сохранить, чтобы сервер понимал, каким алгоритмом потом проверять подписи.
const algorithm = response.getPublicKeyAlgorithm();
Список значений алгоритмов можно подсмотреть в отдельном месте: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
Важно: WebAuthn работает только в HTTPS.
После создания публичного ключа через navigator.credentials.create() можно получить объект ответа и вытащить из него данные, которые понадобятся серверу для регистрации нового credential.
const credential = await navigator.credentials.create({ publicKey: { challenge, rp: { name: 'Example App', id: 'example.com', }, user: { id: userId, name: 'user@example.com', displayName: 'User Example', }, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }, ], authenticatorSelection: { userVerification: 'preferred', }, timeout: 60000, attestation: 'none', }, }); const response = credential.response; const authenticatorData = response.getAuthenticatorData(); const publicKey = response.getPublicKey(); const publicKeyAlgorithm = response.getPublicKeyAlgorithm();
Дальше эти данные обычно отправляют на сервер вместе с credential.id и clientDataJSON:
await fetch('/api/webauthn/register', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: credential.id, rawId: arrayBufferToBase64Url(credential.rawId), type: credential.type, clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON), authenticatorData: arrayBufferToBase64Url(authenticatorData), publicKey: publicKey ? arrayBufferToBase64Url(publicKey) : null, publicKeyAlgorithm, }), });
В реальной жизни это пригодится при регистрации входа без пароля. Например, пользователь нажимает «Создать passkey», подтверждает действие через Touch ID, Face ID, Windows Hello или аппаратный ключ, а сайт сохраняет публичный ключ на сервере:
async function registerPasskey() { const options = await fetch('/api/webauthn/register/options') .then((response) => response.json()); const credential = await navigator.credentials.create({ publicKey: options, }); const attestationResponse = credential.response; const publicKey = attestationResponse.getPublicKey(); if (!publicKey) { throw new Error('Не удалось получить публичный ключ'); } await fetch('/api/webauthn/register/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ credentialId: credential.id, publicKey: arrayBufferToBase64Url(publicKey), algorithm: attestationResponse.getPublicKeyAlgorithm(), authenticatorData: arrayBufferToBase64Url( attestationResponse.getAuthenticatorData(), ), clientDataJSON: arrayBufferToBase64Url( attestationResponse.clientDataJSON, ), }), }); }
Ещё один пример — корпоративная админка, где нужно добавить второй фактор входа. Пользователь уже вошёл по паролю, открывает раздел безопасности и привязывает аппаратный ключ:
async function addSecurityKey() { const creationOptions = await fetch('/security-keys/options') .then((response) => response.json()); const credential = await navigator.credentials.create({ publicKey: creationOptions, }); const { response } = credential; await fetch('/security-keys', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ credentialId: credential.id, publicKey: arrayBufferToBase64Url(response.getPublicKey()), algorithm: response.getPublicKeyAlgorithm(), }), }); }
И пример для личного кабинета: включение входа по passkey. Клиентская часть создаёт credential, а сервер сохраняет публичный ключ. При следующем входе сервер отправит challenge, браузер подпишет его приватным ключом, а сервер проверит подпись по сохранённому публичному ключу.
const enablePasskeyButton = document.querySelector('.js-enable-passkey'); enablePasskeyButton.addEventListener('click', async () => { const options = await getPasskeyCreationOptions(); const credential = await navigator.credentials.create({ publicKey: options, }); const response = credential.response; await savePasskey({ credentialId: credential.id, publicKey: response.getPublicKey(), algorithm: response.getPublicKeyAlgorithm(), authenticatorData: response.getAuthenticatorData(), clientDataJSON: response.clientDataJSON, }); });
Эта фича не добавляет WebAuthn с нуля и не заменяет серверную проверку. Она просто делает этап регистрации удобнее: браузер даёт прямой доступ к публичному ключу, алгоритму и данным аутентификатора, а разработчику больше не нужно вручную разбирать attestationObject ради базовых данных.
- https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse
- https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/getPublicKey
- https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/getPublicKeyAlgorithm
- https://www.iana.org/assignments/cose/cose.xhtml#algorithms
isWellFormed() и toWellFormed() — это методы строк в JavaScript для работы с корректностью UTF-16. Они помогают проверить, есть ли в строке «одинокие суррогаты» — части Unicode-символов, которые должны идти парой, но по какой-то причине оказались в строке отдельно. Такие строки считаются некорректно сформированными.
Обычно разработчик не думает о таких деталях: строки просто приходят из форм, API, файлов, URL или пользовательского ввода. Но иногда в них может попасть битая Unicode-последовательность. Например, из-за неправильной обработки эмодзи, обрезки строки по length, старого API или повреждённых данных.
isWellFormed() позволяет проверить строку и получить true или false, а toWellFormed() возвращает новую строку, где все проблемные одиночные суррогаты заменены на символ � — Unicode replacement character U+FFFD.
Метод isWellFormed() вызывается у строки без аргументов:
str.isWellFormed()
Он возвращает
true
Если строка корректно сформирована и не содержит одиночных суррогатов.
false
если в строке есть хотя бы один одиночный суррогат.
Пример:
'Привет'.isWellFormed(); // true 'ab\uD800'.isWellFormed(); // false
Метод toWellFormed() тоже вызывается у строки без аргументов:
str.toWellFormed()
Он возвращает новую строку. Если исходная строка уже была корректной, вернётся её копия. Если в строке были одиночные суррогаты, они будут заменены на �.
Пример:
'ab\uD800'.toWellFormed(); // 'ab�' 'Привет'.toWellFormed(); // 'Привет'
Важное отличие:
const value = 'ab\uD800'; value.isWellFormed(); // false value.toWellFormed(); // 'ab�'
isWellFormed() отвечает на вопрос: «Со строкой всё хорошо?»
toWellFormed() делает строку безопаснее для дальнейшей обработки.
Пользователь вводит поисковый запрос, а приложение собирает URL для страницы результатов. Перед кодированием запроса можно привести строку к корректному виду:
const input = document.querySelector('#search'); function buildSearchUrl() { const query = input.value.toWellFormed(); return `/search?q=${encodeURIComponent(query)}`; }
Другой сценарий — валидация данных перед отправкой формы. Если приложение не хочет молча заменять проблемные символы, можно показать ошибку:
function validateComment(comment) { if (!comment.isWellFormed()) { return 'В тексте есть некорректные Unicode-символы. Попробуйте удалить последний введённый символ.'; } return null; }
Последний пример — логирование или отправка данных в API. Если строка пришла из внешнего источника и приложение не должно падать из-за битого Unicode, её можно нормализовать перед отправкой:
async function sendMessage(text) { await fetch('/api/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: text.toWellFormed(), }), }); }
isWellFormed() нужен, когда приложение хочет проверить строку и решить, что делать дальше. toWellFormed() нужен, когда приложение хочет получить безопасную строку для дальнейшей обработки: кодирования URL, отправки в API, логирования или работы с текстом из внешних источников.
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/isWellFormed - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed
ARIA attribute reflection — это возможность работать с ARIA-атрибутами как с JavaScript-свойствами DOM-элемента. Вместо setAttribute() и getAttribute() можно писать более привычный JS-код: element.ariaLabel, element.ariaExpanded, element.ariaPressed, element.role и так далее.
Раньше для изменения ARIA-состояния обычно писали так:
button.setAttribute('aria-pressed', 'true');
Теперь можно так:
button.ariaPressed = 'true';
Для простых ARIA-атрибутов это в первую очередь удобство и читаемость. Но у фичи есть более важная часть: работа с ARIA-связями между элементами. Например, aria-labelledby, aria-describedby или aria-activedescendant исторически завязаны на id: нужно было дать одному элементу уникальный id, а потом сослаться на него строкой. Это неудобно для динамических интерфейсов и может ломаться в случаях вроде Shadow DOM.
ARIA reflection позволяет в некоторых случаях работать не со строкой id, а напрямую с элементом:
input.ariaLabelledByElements = [label];
То есть JavaScript получает более естественный способ описывать доступные связи: «это поле подписано вот этим элементом», а не «это поле подписано элементом с таким-то id».
Для простых ARIA-атрибутов используется camelCase-свойство на DOM-элементе.
Было:
element.setAttribute('role', 'button'); element.setAttribute('aria-pressed', 'true'); element.setAttribute('aria-disabled', 'false');
Стало:
element.role = 'button'; element.ariaPressed = 'true'; element.ariaDisabled = 'false';
Значения ARIA-свойств обычно остаются строками, потому что они отражают HTML-атрибуты. Поэтому для aria-pressed или aria-disabled используется не булево значение true, а строку 'true':
button.ariaPressed = 'true'; button.ariaDisabled = 'false';
Отражение работает в обе стороны. Если установить атрибут через HTML или setAttribute(), значение можно прочитать через JS-свойство:
element.setAttribute('aria-atomic', 'true'); console.log(element.ariaAtomic); // 'true'
И наоборот: если установить свойство, в DOM появится соответствующий атрибут:
button.ariaPressed = 'true'; console.log(button.getAttribute('aria-pressed')); // 'true'
Для ARIA-связей есть свойства, которые могут принимать элементы или массивы элементов., например ariaActiveDescendantElement для одного элемента и ariaDescribedByElements для списка элементов.
listbox.ariaActiveDescendantElement = option; input.ariaDescribedByElements = [hint, error];
Если связь задаётся через обычный HTML-атрибут, её тоже можно прочитать через новое свойство:
<div id="fruitbowl" role="listbox" aria-activedescendant="apple"> <div id="apple">Apple</div> </div>
console.log(fruitbowl.ariaActiveDescendantElement === apple); // true
Для атрибутов со списком IDREF, например aria-labelledby или aria-describedby, свойство работает как массив элементов:
<span id="label">Email</span> <span id="hint">We will not share it</span> <input aria-labelledby="label" aria-describedby="hint">
console.log(input.ariaLabelledByElements); // [label] console.log(input.ariaDescribedByElements); // [hint]
Есть важный нюанс: массивы, которые возвращаются из таких свойств, не обязательно будут тем же самым объектом, который вы записали. Поэтому не стоит сравнивать сами массивы через ===; лучше сравнивать их содержимое.
const elements = [hint, error]; input.ariaDescribedByElements = elements; console.log(input.ariaDescribedByElements === elements); // false console.log(input.ariaDescribedByElements[0] === hint); // true
Вместо ручной установки role и aria-pressed через setAttribute() можно использовать свойства DOM-элемента.
const button = document.querySelector('.toggle'); button.role = 'button'; button.ariaPressed = 'false'; button.addEventListener('click', () => { const isPressed = button.ariaPressed === 'true'; button.ariaPressed = isPressed ? 'false' : 'true'; });
Пример со связью через aria-labelledby. Раньше нужно было следить, чтобы у подписи был уникальный id, а потом передавать этот id строкой:
<span id="street-label">Street name</span> <input aria-labelledby="street-label">
С ARIA reflection связь можно задать через элемент:
const input = document.querySelector('input'); const label = document.querySelector('.street-label'); input.ariaLabelledByElements = [label];
Работа с ARIA из JavaScript становится удобнее: простые состояния можно менять через свойства, а связи между элементами — задавать напрямую через DOM-элементы, а не через строки с id.
- https://wicg.github.io/aom/aria-reflection-explainer.html
- https://developer.mozilla.org/ru/docs/Web/API/Element#instance_properties
- https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties
В следующем месяце будет всего пять новых фич. До встречи в июне.
Привет. Я также пишу про CSS-спецификации простым языком. Веду фронтенд дайджест. Ежедневно исследую CSS. Создаю инструменты. Об этом всём я пишу в телеграм(пока что) канале, блоге и других ресурсах. Телеграм является входной точкой. Там без ереси, только код и живые встречи - https://t.me/greatAttractorCode