javascript

Baseline: апрель 2026

  • суббота, 2 мая 2026 г. в 00:00:02
https://habr.com/ru/articles/1028616/

Обзор на браузерные 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