Новинки веб-платформы в сентябре 2026: картинки для темы, прозрачность и отмена запросов
- 5 октября 2026
Сентябрь принёс CSS-функции для тёмной темы и расчётов. А группировка данных и объединение сигналов отмены перешли в широко доступные возможности. Начнём с оформления карточки: здесь результат видно сразу.
Выпуск собран по сентябрьским заметкам Web Features Explorer, поддержка сверена на 9 октября 2026 года. Выбрали то, что пригодится в вёрстке и браузерном JavaScript.
У новинок бывают разные даты. Newly Available означает, что возможность поддерживают актуальные основные браузеры. Через 30 месяцев она получает статус Widely Available. Поэтому широко доступная функция могла появиться несколько лет назад; старые браузеры всё равно нужно учитывать отдельно.
Что получили актуальные браузеры
light-dark(): фон карточки подстраивается под тему
У light-dark() появилась форма для изображений. Теперь два аргумента могут быть градиентами или ссылками на файлы: первый для светлой темы, второй для тёмной. С 14 сентября эта форма получила Newly Available.
Соберём карточку с декоративным фоном. Важный текст остаётся в HTML:
<article class="course-card">
<h2>Учебный проект: каталог</h2>
<p>Карточки товаров, фильтры и адаптивная сетка.</p>
</article>:root {
color-scheme: light dark;
}
.course-card {
padding: 1.5rem;
border-radius: 1rem;
color: #241a3d;
background-image: linear-gradient(135deg, #f4efff, #ffffff);
}
@media (prefers-color-scheme: dark) {
.course-card {
color: #f7f1ff;
background-image: linear-gradient(135deg, #251638, #14101c);
}
}
@supports (background-image: light-dark(linear-gradient(white, white), linear-gradient(black, black))) {
.course-card {
color: light-dark(#241a3d, #f7f1ff);
background-image: light-dark(
linear-gradient(135deg, #f4efff, #ffffff),
linear-gradient(135deg, #251638, #14101c)
);
}
}В браузере с поддержкой выбор делает light-dark(). В остальных остаются обычный фон и медиавыражение. color-scheme: light dark сообщает браузеру, что страница подготовлена к обеим темам. Без него переключение не заработает так, как ожидается.
Проверяйте именно вариант с изображениями. Поддержка light-dark(white, black) для цветов появилась раньше и сама по себе ничего не говорит о градиентах. Вместо градиентов можно передать два url(), но смешать цвет и изображение в одной паре нельзя. Смысловую фотографию товара лучше оставить в <img> с описанием.
Поддержка — в карточке light-dark() image values, синтаксис — в CSS Color Level 5.
alpha(): полупрозрачный фон из основного цвета
Для метки «Новинка» нужен бледный вариант фирменного цвета. Можно хранить его отдельной переменной, но тогда при смене цвета придётся помнить о второй записи. alpha() берёт исходный цвет и меняет его альфа-канал — прозрачность.
<span class="label">Новинка</span>:root {
--brand: #5e2cd0;
}
.label {
padding: 0.25rem 0.75rem;
color: var(--brand);
background-color: rgb(94 44 208 / 15%);
border-radius: 1rem;
}
@supports (color: alpha(from red / 15%)) {
.label {
background-color: alpha(from var(--brand) / 15%);
}
}Ключевое слово from указывает исходный цвет, после слеша стоит новая непрозрачность. Здесь фон получил 15%, текст сохранил свой цвет. Свойство opacity на всей метке изменило бы также текст, поэтому для этой задачи выбираем alpha().
Цветовые каналы сохраняются. Если нужно уменьшить уже заданную непрозрачность вдвое, используйте alpha(from var(--brand) / calc(alpha * 0.5)). Фиксированное 15% задаёт новое значение независимо от старого. После смены фона проверяйте контраст текста: прозрачный цвет смешивается с тем, что находится под ним.
Newly Available с 14 сентября: карточка alpha(). Подробности — в документации MDN.
progress(): значение между началом и концом диапазона
Сколько места карточка прошла между узким и широким экраном? progress() возвращает это отношение числом от нуля до единицы. В сентябре, с первого числа, функция получила Newly Available.
Для заголовка зададим размер от 1.25rem до 2rem при ширине окна от 320 до 960 пикселей:
<h2 class="catalog-title">Каталог учебных проектов</h2>.catalog-title {
font-size: clamp(1.25rem, calc(0.875rem + 1.875vw), 2rem);
}
@supports (opacity: progress(1, 0, 2)) {
.catalog-title {
font-size: calc(
1.25rem + progress(100vw, 320px, 960px) * 0.75rem
);
}
}При ширине 640 пикселей результат progress() равен 0.5: окно находится посередине диапазона. В CSS получается 1.625rem. За границами диапазона обычная форма функции ограничивает результат нулём или единицей. Аргументы должны иметь совместимый тип: в примере все три — длины.
Обычный clamp() остаётся запасным решением; при размере 1rem, равном 16 пикселям, он даёт ту же зависимость. progress() полезнее там, где один коэффициент нужен для нескольких расчётов. Ради одного размера текста менять рабочий clamp() мы бы не стали.
Название может сбить с толку: загрузку файла функция сама не отслеживает. Для шкалы загрузки нужно получить реальные данные в JavaScript и передать значение в CSS.
Примеры и формула — в MDN про progress(), даты — в Explorer.
Что стало широко доступным
Object.groupBy(): карточки раскладываются по категориям
Данные каталога часто приходят одним массивом. Для секций «Вёрстка» и «JavaScript» нужны отдельные группы. Object.groupBy() строит их по результату функции, которую вы передаёте:
const projects = [
{ title: 'Каталог', category: 'html' },
{ title: 'Фильтры', category: 'javascript' },
{ title: 'Портфолио', category: 'html' }
];
function groupProjects(items) {
if (typeof Object.groupBy === 'function') {
return Object.groupBy(items, (item) => item.category);
}
return items.reduce((groups, item) => {
(groups[item.category] ??= []).push(item);
return groups;
}, Object.create(null));
}
const groups = groupProjects(projects);
console.log(groups.html.map((item) => item.title));
// ['Каталог', 'Портфолио']Ключи групп здесь строковые. Когда ключом нужен сам объект, посмотрите на Map.groupBy(). Новые массивы групп содержат ссылки на исходные элементы: изменение объекта будет видно и в исходном массиве. Копию содержимого метод не делает.
У результата Object.groupBy() нет прототипа. Для проверки наличия группы используйте Object.hasOwn(groups, 'html'), а не метод groups.hasOwnProperty(). Запасная ветка в примере создаёт объект с таким же устройством.
Группировка массивов получила Widely Available 5 сентября. См. карточку Explorer и справку Object.groupBy().
AbortSignal.any(): запрос отменяется по двум причинам
Каталог может долго ждать ответ. Пользователь должен иметь возможность отменить запрос, а приложение — прекратить ожидание по таймеру. AbortSignal.any() объединяет несколько сигналов. Первый сработавший сигнал отменяет общий.
Пример ожидает catalog.json в той же папке. Создайте файл с содержимым [{"title":"Каталог"}] и откройте страницу через локальный сервер:
<button id="load" type="button">Загрузить каталог</button>
<button id="cancel" type="button" disabled>Отменить</button>
<pre id="result" aria-live="polite"></pre>Подключите JavaScript после разметки кнопок, чтобы элементы уже были на странице:
const loadButton = document.querySelector('#load');
const cancelButton = document.querySelector('#cancel');
const result = document.querySelector('#result');
loadButton.addEventListener('click', async () => {
const userCancel = new AbortController();
const deadline = new AbortController();
const fallback = new AbortController();
const stopByUser = () => userCancel.abort(
new DOMException('Пользователь отменил запрос', 'AbortError')
);
const forward = (event) => fallback.abort(event.target.reason);
userCancel.signal.addEventListener('abort', forward, { once: true });
deadline.signal.addEventListener('abort', forward, { once: true });
const signal = typeof AbortSignal.any === 'function'
? AbortSignal.any([userCancel.signal, deadline.signal])
: fallback.signal;
const timer = setTimeout(() => deadline.abort(
new DOMException('Время ожидания вышло', 'TimeoutError')
), 5000);
loadButton.disabled = true;
cancelButton.disabled = false;
cancelButton.addEventListener('click', stopByUser, { once: true });
result.textContent = 'Загружаем каталог';
try {
const response = await fetch('./catalog.json', { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const projects = await response.json();
result.textContent = JSON.stringify(projects, null, 2);
} catch (error) {
result.textContent = signal.aborted
? signal.reason.message
: `Не удалось загрузить: ${error.message}`;
} finally {
clearTimeout(timer);
cancelButton.removeEventListener('click', stopByUser);
userCancel.signal.removeEventListener('abort', forward);
deadline.signal.removeEventListener('abort', forward);
loadButton.disabled = false;
cancelButton.disabled = true;
}
});Здесь два контроллера задают разные причины: ручная отмена и пять секунд ожидания. Запасной контроллер повторяет объединение для браузера без any(). Обработчики снимаются после запроса, таймер очищается.
Отмена ожидания в браузере не гарантирует отмену работы на сервере. Кроме того, общий сигнал не отменяет остальные входные сигналы; поэтому таймер нужно убрать самим. Для проверки откройте замедление сети в инструментах разработчика и нажмите «Отменить» до ответа.
Widely Available с 19 сентября: AbortSignal.any() в Explorer. Поведение общего сигнала описано в MDN.
Что попробовать в своём проекте
Начните с alpha() для декоративного фона метки: изменение небольшое, результат можно проверить рядом с исходным вариантом. Для каталога с запросами полезнее сначала добавить отмену и понятное сообщение пользователю.
Перед переносом примера:
- проверьте поддержку конкретной формы функции в браузерах проекта;
- выключите новую ветку и убедитесь, что запасной вариант работает;
- сравните контраст в обеих темах и размер текста при увеличении страницы;
- для запроса проверьте успешный ответ, HTTP-ошибку и отмену во время ожидания.
Меняется ли читаемость метки на другом фоне? Остаются ли после отмены запроса активные таймеры и обработчики? Проверьте эти случаи в учебном проекте до добавления следующей новинки.
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.