Обзор на браузерные API, которые стали Widely available в апреле 2026. Раз в месяц я буду вам напоминать, что вы уже можете использовать в проде.
Каждый месяц выходят новые CSS-свойства, HTML-атрибуты, JavaScript-методы и WebAPI, но применять в проде мы их конечно же не будем.
2.5 года назад также каждый месяц выходили новые фичи в браузере, а вот их уже пора начинать применять.
Как мы понимаем, что уже можно использовать в проде?
У каждой компании, да что уж там компании, у каждой команды в компании своя методика принятия решения о внедрении той или иной фичи в проекте.
Общий же сценарий выглядит так:
- Посмотрели в пользовательские метрики. Поняли какими браузерами и их версиями в основном пользуются пользователи проекта;
- Заглянули в caniuse и поняли, какие фичи уже поддерживаются большинством браузеров;
- Приняли решение о внедрении той или иной фичи в проект.
Какие-то команды позволяют себе указывать правило "последние три версии браузеров". У других специфика проекта, что проект работает исключительно на iPad с Safari. Сами понимаете, все мы разные и требования разные, и у каждого свой подход.
Baseline - позволяет немного упростить процесс принятия решения о внедрении той или иной фичи в проект. Если фича Widely available значит фича уже как минимум есть во всех основных браузерах как минимум стабильно используются последние 2.5 года.
Какие фичи в вебе стали Widely available в апреле 2026?
- <search>
- Web authentication easy public key access
- String isWellFormed() and toWellFormed()
- ARIA attribute reflection
1. <search>
<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
2. Web authentication easy public key access
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
3. String isWellFormed() and toWellFormed()
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
4. ARIA attribute reflection
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
-Источник