Isolated declarations — ускорение больших monorepo
В TypeScript есть флаг
Проблема простая:
в больших monorepo генерация
может становиться узким местом.
TypeScript часто должен анализировать соседние файлы,
чтобы понять, какие декларации вывести.
На маленьком проекте это почти незаметно.
Что делает isolatedDeclarations
чтобы декларации можно было генерировать
по файлам независимо.
Из-за этого TypeScript чаще требует явные типы.
Было:
Лучше так:
Почему это важно
Когда проект растёт:
👉 TypeScript начинает сильнее зависеть от соседних файлов
👉 инкрементальная сборка замедляется
👉 генерация типов становится дорогой
Где это особенно полезно
👉 большие monorepo
👉 библиотеки
👉 project references
👉 параллельная сборка
👉 CI, где каждая минута стоит денег
Главный trade-off
Ты немного платишь:
👉 более явными типами
👉 меньшим type inference
👉 дополнительным boilerplate
Но взамен получаешь:
👉 более быстрые сборки
👉 стабильный compile pipeline
👉 меньше скрытой сложности
Главная мысль
В TypeScript есть флаг
isolatedDeclarations.
Он нужен не для красоты типов,
а для скорости.
Проблема простая:
в больших monorepo генерация
.d.tsможет становиться узким местом.
TypeScript часто должен анализировать соседние файлы,
чтобы понять, какие декларации вывести.
На маленьком проекте это почти незаметно.
На большом — начинает болеть.
Что делает isolatedDeclarations
isolatedDeclarations заставляет писать код так,чтобы декларации можно было генерировать
по файлам независимо.
Из-за этого TypeScript чаще требует явные типы.
Было:
export function getUser() {
return { id: 1, name: 'Alex' }
}
Лучше так:
type User = {
id: number
name: string
}
export function getUser(): User {
return { id: 1, name: 'Alex' }
}
Меньше магии для компилятора —
быстрее и предсказуемее сборка.
Почему это важно
Когда проект растёт:
👉 TypeScript начинает сильнее зависеть от соседних файлов
👉 инкрементальная сборка замедляется
👉 генерация типов становится дорогой
Изоляция помогает компилятору работать параллельно и проще.
Где это особенно полезно
👉 большие monorepo
👉 библиотеки
👉 project references
👉 параллельная сборка
👉 CI, где каждая минута стоит денег
Главный trade-off
Ты немного платишь:
👉 более явными типами
👉 меньшим type inference
👉 дополнительным boilerplate
Но взамен получаешь:
👉 более быстрые сборки
👉 стабильный compile pipeline
👉 меньше скрытой сложности
Главная мысль
Это хороший пример взрослого engineering trade-off:
чуть больше явности в коде
ради скорости и предсказуемости системы.
❤1👍1
Recursive type limits — почему TS иногда «умирает»
В TypeScript можно написать тип,
который выглядит красиво,
но заставляет компилятор страдать.
Например:
👉 глубокий
👉 парсинг строк на уровне типов
👉 сложные conditional types
👉
На маленьком примере всё работает.
Почему так происходит
TypeScript не вычисляет типы «бесплатно».
Каждый:
👉 conditional type
👉 union
👉 recursive шаг
нужно реально посчитать.
А если тип разворачивается слишком глубоко,
компилятор упирается в лимиты.
Отсюда знакомое:
Часто это сигнал,
что типовая модель стала слишком умной.
Где обычно всё ломается
Особенно опасны:
👉 рекурсивные mapped types
👉 огромные union’ы
👉 type-level parser’ы
👉 deeply nested generics
👉 utility types поверх utility types
Что обычно помогает
👉 не делать type-level акробатику без нужды
👉 ограничивать глубину рекурсии
👉 разбивать типы на более простые
👉 добавлять явные промежуточные типы
👉 не тащить сложные generic-типы в публичный API
Почему это важно
Сложные типы бьют не только по компиляции.
Они ухудшают:
👉 autocomplete
👉 responsiveness IDE
👉 читаемость кода
👉 onboarding новых людей
Главная мысль
Хороший TypeScript —
это не когда типы поражают воображение.
В TypeScript можно написать тип,
который выглядит красиво,
но заставляет компилятор страдать.
Особенно когда начинаются рекурсивные типы.
Например:
👉 глубокий
DeepPartial 👉 парсинг строк на уровне типов
👉 сложные conditional types
👉
infer внутри infer На маленьком примере всё работает.
В реальном проекте IDE внезапно начинает думать по 5 секунд.
Почему так происходит
TypeScript не вычисляет типы «бесплатно».
Каждый:
👉 conditional type
👉 union
👉 recursive шаг
нужно реально посчитать.
А если тип разворачивается слишком глубоко,
компилятор упирается в лимиты.
Отсюда знакомое:
Type instantiation is excessively deep
and possibly infinite
И это не всегда баг TypeScript.
Часто это сигнал,
что типовая модель стала слишком умной.
Где обычно всё ломается
Особенно опасны:
👉 рекурсивные mapped types
👉 огромные union’ы
👉 type-level parser’ы
👉 deeply nested generics
👉 utility types поверх utility types
Типы начинают взрываться комбинаторно.
Что обычно помогает
👉 не делать type-level акробатику без нужды
👉 ограничивать глубину рекурсии
👉 разбивать типы на более простые
👉 добавлять явные промежуточные типы
👉 не тащить сложные generic-типы в публичный API
Почему это важно
Сложные типы бьют не только по компиляции.
Они ухудшают:
👉 autocomplete
👉 responsiveness IDE
👉 читаемость кода
👉 onboarding новых людей
Иногда самый дорогой runtime —
это compile time.
Главная мысль
Хороший TypeScript —
это не когда типы поражают воображение.
Хороший TypeScript —
это когда их можно понять через полгода,
а IDE при этом не превращается в обогреватель.
❤1
ES2025: Импорт JSON-файлов как модулей
Введение
С выходом
Синтаксис импорта JSON-модулей
Для импорта JSON-файла используется ключевое слово
Пример использования
Рассмотрим пример импорта конфигурационного файла
❗️Добавление поддержки импорта JSON-файлов как модулей в
Источники
JSON Modules Can Now Be Imported in JavaScript in All Modern Browsers, CSS Modules to Follow.
New Features in ES2025 – BooleanBuffer.
Введение
С выходом
ECMAScript 2025 (ES2025) разработчики получили возможность напрямую импортировать JSON-файлы как модули в JavaScript-коде. Это упрощает работу с конфигурационными данными и другими статическими ресурсами, представленными в формате JSON.Синтаксис импорта JSON-модулей
Для импорта JSON-файла используется ключевое слово
import с указанием атрибута with { type: 'json' }. Это гарантирует, что импортируемый файл будет обработан как JSON-модуль.Пример использования
Рассмотрим пример импорта конфигурационного файла
config.json и обращения к его свойствам в коде.import config from './config.json' with { type: 'json' };
.log(config.apiUrl); // Выводит значение свойства apiUrl из config.json
console.log(config.timeout); // Выводит значение свойства timeout из config.json
❗️Добавление поддержки импорта JSON-файлов как модулей в
ES2025 упрощает работу с данными в формате JSON, делая код более чистым и понятным.Источники
JSON Modules Can Now Be Imported in JavaScript in All Modern Browsers, CSS Modules to Follow.
New Features in ES2025 – BooleanBuffer.
InfoQ
JSON Modules Can Now Be Imported in JavaScript in All Modern Browsers, CSS Modules to Follow
Thomas Steiner, developer relations engineer at Google, recently published a blog post announcing that JSON module scripts were now available in all modern browsers. Developers using the latest version of modern browsers can now directly import JSON modules…
Variadic tuple types — сложные сигнатуры без боли
До variadic tuple types
многие сложные сигнатуры в TypeScript
выглядели как наказание.
Особенно:
👉 curry
👉 compose
👉 middleware
👉 typed event emitter
👉 любые функции с «прокинь аргументы дальше»
Как было раньше
Обычно появлялись:
👉 overload на overload
👉 ручные tuple-типы
👉 тонны дублирования
Типы быстро превращались
в нечитаемую простыню.
Что изменили variadic tuples
С их появлением стало намного проще
работать с остаточными аргументами на уровне типов.
Например:
Или собирать сигнатуры:
Почему это важно
На практике это одна из тех TS-фич,
которые реально упростили жизнь библиотекам.
Без variadic tuples:
👉 Redux middleware typings
👉 router APIs
👉 compose/curry utilities
были бы ещё страшнее.
Где начинается тёмная магия
Проблемы начинаются,
когда variadic tuples комбинируют с:
👉
👉 recursive types
👉 conditional types
IDE начинает тормозить,
ошибки становятся нечитаемыми,
а compile time — расти.
Главная мысль
Variadic tuple types —
это действительно мощная фича.
До variadic tuple types
многие сложные сигнатуры в TypeScript
выглядели как наказание.
Особенно:
👉 curry
👉 compose
👉 middleware
👉 typed event emitter
👉 любые функции с «прокинь аргументы дальше»
Приходилось писать overload на overload
и дублировать типы вручную.
Как было раньше
Обычно появлялись:
👉 overload на overload
👉 ручные tuple-типы
👉 тонны дублирования
Типы быстро превращались
в нечитаемую простыню.
Что изменили variadic tuples
С их появлением стало намного проще
работать с остаточными аргументами на уровне типов.
Например:
type Fn<T extends unknown[]> =
(...args: [...T]) => void
Или собирать сигнатуры:
type Append<Args extends unknown[], Arg> =
[...Args, Arg]
Типы наконец научились нормально работать
с «переменным количеством аргументов».
Почему это важно
На практике это одна из тех TS-фич,
которые реально упростили жизнь библиотекам.
Без variadic tuples:
👉 Redux middleware typings
👉 router APIs
👉 compose/curry utilities
были бы ещё страшнее.
Где начинается тёмная магия
Проблемы начинаются,
когда variadic tuples комбинируют с:
👉
infer 👉 recursive types
👉 conditional types
Типовая система очень быстро
превращается в тёмный лес.
IDE начинает тормозить,
ошибки становятся нечитаемыми,
а compile time — расти.
Главная мысль
Variadic tuple types —
это действительно мощная фича.
Главное —
вовремя остановиться
и не превратить типы в отдельный язык программирования.
CSS Anchor Positioning в продакшене: поповеры и тултипы без JS-позиционирования и вечной борьбы с координатами
Popover в кабинете, SaaS-интерфейсе или дизайн-системе часто ломается не из-за UI, а из-за геометрии. Типичная ошибка - тащить в каждый tooltip getBoundingClientRect(), scroll/resize handlers и свой мини layout engine.
Идея
CSS Anchor Positioning позволяет привязать один элемент к другому и описать размещение в CSS:
Actions
Что уходит из JS
* anchor-name объявляет кнопку якорем
* position-anchor связывает overlay с якорем
* position-area задает сторону и выравнивание
* position-try-fallbacks дает браузеру шанс переставить popover, если снизу нет места
В связке с Popover API HTML отвечает за открытие, закрытие и top layer, CSS - за геометрию, JS - за бизнес-логику.
Продакшен-подход
Используйте как progressive enhancement:
Практический совет: оставьте fallback - обычное позиционирование, modal-like поведение или существующий overlay-компонент из дизайн-системы.
Где осторожно
Технология хороша для меню у кнопки, dropdown, tooltip у иконки, inline-подсказок форм. Но для nested-popover, виртуализированных списков, кастомной collision-логики и полной кроссбраузерной поддержки пока рано выкидывать Floating UI или Popper.
Вывод:
CSS Anchor Positioning убирает самый грязный слой overlay-архитектуры - JS-позиционирование, но требует feature detection, fallback и проверки доступности.
Popover в кабинете, SaaS-интерфейсе или дизайн-системе часто ломается не из-за UI, а из-за геометрии. Типичная ошибка - тащить в каждый tooltip getBoundingClientRect(), scroll/resize handlers и свой мини layout engine.
Идея
CSS Anchor Positioning позволяет привязать один элемент к другому и описать размещение в CSS:
Actions
.trigger { anchor-name: --actions; }
.popover {
position: fixed;
position-anchor: --actions;
position-area: bottom center;
position-try-fallbacks: flip-block;
margin-top: 8px;
}Что уходит из JS
* anchor-name объявляет кнопку якорем
* position-anchor связывает overlay с якорем
* position-area задает сторону и выравнивание
* position-try-fallbacks дает браузеру шанс переставить popover, если снизу нет места
В связке с Popover API HTML отвечает за открытие, закрытие и top layer, CSS - за геометрию, JS - за бизнес-логику.
Продакшен-подход
Используйте как progressive enhancement:
@supports (anchor-name: --x) and (position-area: bottom) {
.popover {
position-anchor: --actions;
position-area: bottom center;
}
}
Практический совет: оставьте fallback - обычное позиционирование, modal-like поведение или существующий overlay-компонент из дизайн-системы.
Где осторожно
Технология хороша для меню у кнопки, dropdown, tooltip у иконки, inline-подсказок форм. Но для nested-popover, виртуализированных списков, кастомной collision-логики и полной кроссбраузерной поддержки пока рано выкидывать Floating UI или Popper.
Вывод:
CSS Anchor Positioning убирает самый грязный слой overlay-архитектуры - JS-позиционирование, но требует feature detection, fallback и проверки доступности.
@starting-style + transition-behavior: enter/exit-анимации для dialog и popover без JS-классовВ production-модалках, меню, кабинетах, design systems и SaaS-интерфейсах анимация часто ломалась из-за
display, top layer и быстрых повторных кликов. Типичная ошибка - хранить фазы в JS-классах и ждать transitionend.Что меняется
@starting-style задаёт старт для элемента, который только появился: dialog[open] или :popover-open. transition-behavior: allow-discrete позволяет удержать дискретные display и overlay до конца exit-анимации, включая ::backdrop.dialog,[popover]{
opacity:0;
transform:translateY(12px) scale(.98);
transition:
opacity .2s,
transform .2s,
display .2s allow-discrete,
overlay .2s allow-discrete;
}
dialog[open],
[popover]:popover-open{
opacity:1;
transform:none;
}
@starting-style{
dialog[open],
[popover]:popover-open{
opacity:0;
transform:translateY(12px) scale(.98);
}
}Где trade-off
Состояние остаётся у платформы:
popover может открываться через popovertarget, а для dialog нужны только showModal()/close(). Практический совет: анимируйте opacity и transform, а display/overlay добавляйте только как discrete-свойства для стабильного выхода.Предупреждение
@starting-style нужен для входа, не для выхода. Exit берётся из закрытого состояния. Без allow-discrete элемент исчезнет мгновенно, и регрессия проявится на быстрых кликах, слабых устройствах или в компонентной библиотеке.Вывод:
Для
dialog и popover лучше держать состояние в нативных механизмах, а анимационные фазы описывать в CSS без лишних JS-классов.Совет на ближайшие годы — изучайте ВАЙБ-КОДИНГ
ИИ уже пишет код, чинит баги, генерирует тесты, документацию и помогает запускать продукты быстрее, чем это делали классические команды разработки. И это уже не "будущее когда-нибудь", а реальность, которая меняет рынок уже сегодня
И те, кто научится вайбкодить сейчас, будут увереннее конкурировать на рынке и зарабатывать больше тех, кто по-прежнему делает всё вручную.
Стартовать с нуля поможет канал Вайб-кодинг. Там ребята круглосуточно мониторят более 320 российских и зарубежных источников и публикуют только главное: релизы, инструменты, гайды, курсы и практические кейсы.
Подписывайтесь, нас уже 45 тысяч: @vibecoding_tg
ИИ уже пишет код, чинит баги, генерирует тесты, документацию и помогает запускать продукты быстрее, чем это делали классические команды разработки. И это уже не "будущее когда-нибудь", а реальность, которая меняет рынок уже сегодня
И те, кто научится вайбкодить сейчас, будут увереннее конкурировать на рынке и зарабатывать больше тех, кто по-прежнему делает всё вручную.
Стартовать с нуля поможет канал Вайб-кодинг. Там ребята круглосуточно мониторят более 320 российских и зарубежных источников и публикуют только главное: релизы, инструменты, гайды, курсы и практические кейсы.
Подписывайтесь, нас уже 45 тысяч: @vibecoding_tg
❤1
⚡️
Если на странице много тяжёлых блоков — карточки, статьи, отзывы, секции лендинга, документация — не всегда нужна виртуализация. Иногда достаточно сказать браузеру: «не рендери то, что далеко за экраном».
Для этого есть:
Что происходит:
-
- layout/paint/style откладываются до момента, когда блок приблизится к viewport;
- DOM остаётся на месте: это не виртуализация, элементы не удаляются;
-
Главный нюанс — CLS.
Если просто повесить:
браузер может не знать, сколько места должен занимать пропущенный блок. Когда пользователь доскроллит до него, блок отрендерится, получит реальную высоту и может сдвинуть всё ниже.
Поэтому вместе с
Здесь
Ключевое слово
Практический пример:
Лучше не подбирать один размер для всего. Если блоки сильно отличаются, разбивайте их на классы: обычная карточка, большая карточка, промо-блок, секция с галереей и т.д.
Где это особенно полезно:
- длинные ленты без интерактивной виртуализации;
- страницы документации;
- каталоги с тяжёлыми карточками;
- лендинги с большим количеством секций;
- SSR-страницы, где DOM уже есть, но рендерить всё сразу дорого.
Где быть осторожнее:
- above-the-fold блоки — им это обычно не нужно;
- sticky/anchor-зависимые интерфейсы;
- элементы, размер которых критично влияет на соседей;
- списки на десятки тысяч DOM-узлов — тут виртуализация всё ещё может быть нужна;
- сложные случаи с измерением размеров через JS.
Важно:
Хороший паттерн:
Так это становится progressive enhancement: браузеры с поддержкой получают ускорение, остальные рендерят страницу как обычно.
Итог: для длинных страниц, где не хочется усложнять архитектуру виртуализацией,
content-visibility: auto + contain-intrinsic-size: ускоряем длинные страницы без виртуализации и CLSЕсли на странице много тяжёлых блоков — карточки, статьи, отзывы, секции лендинга, документация — не всегда нужна виртуализация. Иногда достаточно сказать браузеру: «не рендери то, что далеко за экраном».
Для этого есть:
.content-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 480px;
}Что происходит:
-
content-visibility: auto разрешает браузеру пропускать рендеринг offscreen-блоков;- layout/paint/style откладываются до момента, когда блок приблизится к viewport;
- DOM остаётся на месте: это не виртуализация, элементы не удаляются;
-
contain-intrinsic-block-size задаёт резервный размер, пока реальная высота ещё не посчитана.Главный нюанс — CLS.
Если просто повесить:
.card {
content-visibility: auto;
}браузер может не знать, сколько места должен занимать пропущенный блок. Когда пользователь доскроллит до него, блок отрендерится, получит реальную высоту и может сдвинуть всё ниже.
Поэтому вместе с
content-visibility почти всегда нужен intrinsic size:.article-preview {
content-visibility: auto;
contain-intrinsic-block-size: auto 360px;
}Здесь
360px — fallback-оценка высоты до первого рендера. Ключевое слово
auto позволяет браузеру запомнить реальный размер после рендера и дальше использовать уже его.Практический пример:
<main class="feed">
<article class="feed-card">...</article>
<article class="feed-card feed-card--large">...</article>
<article class="feed-card">...</article>
</main>
.feed-card {
content-visibility: auto;
contain-intrinsic-block-size: auto 320px;
}
.feed-card--large {
contain-intrinsic-block-size: auto 560px;
}Лучше не подбирать один размер для всего. Если блоки сильно отличаются, разбивайте их на классы: обычная карточка, большая карточка, промо-блок, секция с галереей и т.д.
Где это особенно полезно:
- длинные ленты без интерактивной виртуализации;
- страницы документации;
- каталоги с тяжёлыми карточками;
- лендинги с большим количеством секций;
- SSR-страницы, где DOM уже есть, но рендерить всё сразу дорого.
Где быть осторожнее:
- above-the-fold блоки — им это обычно не нужно;
- sticky/anchor-зависимые интерфейсы;
- элементы, размер которых критично влияет на соседей;
- списки на десятки тысяч DOM-узлов — тут виртуализация всё ещё может быть нужна;
- сложные случаи с измерением размеров через JS.
Важно:
content-visibility: auto не уменьшает количество DOM-элементов и не отменяет стоимость JS, обработчиков событий или хранения данных в памяти. Он оптимизирует именно рендеринг: layout, paint и часть работы вокруг них.Хороший паттерн:
@supports (content-visibility: auto) {
.long-page-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 600px;
}
}Так это становится progressive enhancement: браузеры с поддержкой получают ускорение, остальные рендерят страницу как обычно.
Итог: для длинных страниц, где не хочется усложнять архитектуру виртуализацией,
content-visibility: auto — дешёвый способ снизить initial rendering cost. Но без contain-intrinsic-size легко получить layout shifts, поэтому эти свойства почти всегда стоит использовать парой.👍1
Cascade Layers в дизайн-системе: reset, vendors и utilities без войн специфичности
В дизайн-системах для SaaS, e-commerce и кабинетов reset, CSS из npm-пакетов, компоненты и utilities быстро начинают конкурировать. Частая ошибка - лечить это через
Контракт слоев
Сначала зафиксируйте каскад явно:
Чем слой правее - тем выше приоритет при одинаковом origin/importance. Специфичность работает внутри слоя, но между слоями сначала побеждает порядок
Production-пример
Теперь
Практичная схема
*
*
*
*
*
*
*
Важные нюансы
Стили вне
Предупреждение: у
Вывод:
В дизайн-системах для SaaS, e-commerce и кабинетов reset, CSS из npm-пакетов, компоненты и utilities быстро начинают конкурировать. Частая ошибка - лечить это через
!important, .foo.foo и порядок импортов.Контракт слоев
Сначала зафиксируйте каскад явно:
@layer reset, vendors, tokens, base, components, utilities, overrides;
Чем слой правее - тем выше приоритет при одинаковом origin/importance. Специфичность работает внутри слоя, но между слоями сначала побеждает порядок
@layer.Production-пример
@import url("./reset.css") layer(reset);
@import url("./datepicker.css") layer(vendors);
@layer components {
.button { background: var(--accent); }
}
@layer utilities {
.bg-danger { background: var(--danger); }
}Теперь
.bg-danger предсказуемо переопределит фон компонента, даже если селектор компонента специфичнее. Не нужно делать .bg-danger.bg-danger или надеяться, что файл utilities подключится последним.Практичная схема
*
reset - нормализация браузерных стилей.*
vendors - datepicker, select, editor, carousel.*
tokens - CSS custom properties и темы.*
base - типографика и дефолты тегов.*
components - button, input, modal, tabs.*
utilities - атомарные override-классы.*
overrides - интеграционные фиксы, которые должны быть видимыми и редкими.Важные нюансы
Стили вне
@layer сильнее обычных layered-стилей. Поэтому договоритесь: весь CSS дизайн-системы живет в слоях, а приложение переопределяет ее через свой верхний слой.@layer app-overrides;
@layer app-overrides {
.checkout .button { min-width: 240px; }
}
Предупреждение: у
!important порядок слоев инвертируется. Не строите архитектуру вокруг него - layers нужны, чтобы снижать потребность в таких патчах.Вывод:
@layer превращает каскад из побочного эффекта сборки в явный API дизайн-системы, повышая предсказуемость layout и снижая визуальные регрессии.Web Locks API в браузере: cross-tab mutex для refresh token, миграций и защиты от гонок
Когда одна сессия открыта в нескольких вкладках, frontend, SPA, SSR-клиенты и SDK начинают делить auth state, storage и кэш. Частая ошибка - защищать refresh token только in-memory single-flight: между вкладками он не работает.
Refresh token как критическая секция
Если 5 вкладок одновременно получили
Важный паттерн
После входа в lock нужно перечитать storage. Пока вкладка ждала, другая уже могла обновить токены. Практический совет: внутри критической секции всегда делайте double-check precondition, а не запускайте refresh сразу после ожидания.
Где еще полезно
Web Locks хорошо ложится на миграции
Ограничения
Lock работает только в пределах origin. Это не distributed lock и не замена серверной идемпотентности. Критическая секция должна быть короткой; для сетевого refresh ставьте timeout. Если
Вывод:
Web Locks API полезен там, где несколько вкладок делят runtime-состояние: он снижает риск гонок, но не отменяет серверные гарантии.
Когда одна сессия открыта в нескольких вкладках, frontend, SPA, SSR-клиенты и SDK начинают делить auth state, storage и кэш. Частая ошибка - защищать refresh token только in-memory single-flight: между вкладками он не работает.
Refresh token как критическая секция
Если 5 вкладок одновременно получили
401, они могут отправить несколько refresh-запросов одним token, получить invalid_grant и перетереть свежие токены старыми. Web Locks API дает mutex на один origin:type Tokens = {
accessToken: string;
refreshToken: string;
};
const LOCK = 'auth:refresh-token';
async function ensureAccessToken(): Promise<string> {
return navigator.locks.request(LOCK, async () => {
const latest: Tokens = await loadTokens();
if (!isExpiring(latest)) {
return latest.accessToken;
}
const next = await refreshTokens(latest.refreshToken);
await saveTokens(next);
return next.accessToken;
});
}Важный паттерн
После входа в lock нужно перечитать storage. Пока вкладка ждала, другая уже могла обновить токены. Практический совет: внутри критической секции всегда делайте double-check precondition, а не запускайте refresh сразу после ожидания.
Где еще полезно
Web Locks хорошо ложится на миграции
IndexedDB/localStorage, одноразовую инициализацию кэша, защиту записи в общий browser storage и координацию фоновых задач между вкладками.Ограничения
Lock работает только в пределах origin. Это не distributed lock и не замена серверной идемпотентности. Критическая секция должна быть короткой; для сетевого refresh ставьте timeout. Если
navigator.locks нет, нужен fallback через BroadcastChannel, storage lease или серверную защиту.Вывод:
Web Locks API полезен там, где несколько вкладок делят runtime-состояние: он снижает риск гонок, но не отменяет серверные гарантии.
CSS Scroll-Driven Animations уже можно аккуратно тащить в продакшен — особенно для двух задач, где раньше часто ставили
1. reading progress bar
2. sticky-reveal секции
Главная идея: прогресс анимации привязан не ко времени, а к скроллу.
или к появлению конкретного блока во viewport:
То есть браузер сам считает progress timeline, а мы описываем только CSS-анимацию.
Пример: progress bar + sticky-reveal без JS
Что важно для продакшена:
• Заворачивать в
Scroll-Driven Animations пока не стоит считать безусловным baseline. Делайте progressive enhancement: без поддержки пользователь просто видит обычный контент.
• Не прятать контент по умолчанию
Плохой паттерн:
А потом раскрывать только внутри scroll-animation. В браузере без поддержки блок останется невидимым. Лучше базово показывать контент, а стартовое состояние задавать внутри
• Анимировать дешёвые свойства
•
Для визуальных reveal/progress эффектов CSS часто проще и чище. Но если нужно грузить данные, отправлять аналитику, управлять состоянием приложения или синхронизироваться с бизнес-логикой — это всё ещё зона JS.
• Для sticky-reveal лучше таймлайн секции, а не самого sticky-элемента
Sticky-элемент может визуально «залипать», поэтому удобнее повесить
Итог: для progress bars и декоративных sticky-reveal эффектов Scroll-Driven Animations позволяют убрать лишний JS, не плодить observers/listeners и оставить поведение декларативным. Главное — progressive enhancement,
IntersectionObserver или scroll-listener:1. reading progress bar
2. sticky-reveal секции
Главная идея: прогресс анимации привязан не ко времени, а к скроллу.
animation-timeline: scroll(root block);
или к появлению конкретного блока во viewport:
animation-timeline: --section;
animation-range: entry 15% cover 45%;
То есть браузер сам считает progress timeline, а мы описываем только CSS-анимацию.
Пример: progress bar + sticky-reveal без JS
<div class="read-progress"></div>
<section class="feature">
<div class="feature__sticky">
<h2>Scroll-driven reveal</h2>
<p>Блок раскрывается по мере прохождения секции.</p>
</div>
</section>
.read-progress {
position: fixed;
z-index: 1000;
inset: 0 0 auto;
block-size: 3px;
background: #7c3aed;
transform-origin: 0 50%;
transform: scaleX(0);
display: none;
}
.feature { min-block-size: 180vh; }
.feature__sticky {
position: sticky;
top: 20svh;
opacity: 1;
transform: none;
}
@supports (animation-timeline: scroll()) {
.read-progress {
display: block;
animation: progress linear both;
animation-timeline: scroll(root block);
}
@keyframes progress {
to { transform: scaleX(1); }
}
.feature {
view-timeline-name: --feature;
view-timeline-axis: block;
}
.feature__sticky {
opacity: 0;
transform: translateY(24px) scale(.98);
animation: stickyReveal linear both;
animation-timeline: --feature;
animation-range: entry 15% cover 45%;
will-change: opacity, transform;
}
@keyframes stickyReveal {
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.read-progress,
.feature__sticky {
animation: none;
}
.feature__sticky {
opacity: 1;
transform: none;
}
}
}Что важно для продакшена:
• Заворачивать в
@supports Scroll-Driven Animations пока не стоит считать безусловным baseline. Делайте progressive enhancement: без поддержки пользователь просто видит обычный контент.
• Не прятать контент по умолчанию
Плохой паттерн:
.card {
opacity: 0;
}А потом раскрывать только внутри scroll-animation. В браузере без поддержки блок останется невидимым. Лучше базово показывать контент, а стартовое состояние задавать внутри
@supports.• Анимировать дешёвые свойства
transform и opacity — ок. height, top, margin, layout-зависимые штуки — осторожно. Scroll-driven не делает дорогую анимацию бесплатной.•
IntersectionObserver всё ещё нужен не всегда, но и не умер Для визуальных reveal/progress эффектов CSS часто проще и чище. Но если нужно грузить данные, отправлять аналитику, управлять состоянием приложения или синхронизироваться с бизнес-логикой — это всё ещё зона JS.
• Для sticky-reveal лучше таймлайн секции, а не самого sticky-элемента
Sticky-элемент может визуально «залипать», поэтому удобнее повесить
view-timeline-name на родительскую секцию, а анимировать внутренний sticky-блок через named timeline.Итог: для progress bars и декоративных sticky-reveal эффектов Scroll-Driven Animations позволяют убрать лишний JS, не плодить observers/listeners и оставить поведение декларативным. Главное — progressive enhancement,
prefers-reduced-motion и нормальный fallback.❤2
CSS Subgrid в продакшене: выравниваем вложенные формы и карточки без дублирования сеток
В кабинетах, checkout, SaaS-формах и дизайн-системах часто нужно, чтобы вложенные секции попадали в колонки общего layout. Типичная ошибка - копировать
Где помогает subgrid
Production-смысл
Так сетка описывается один раз в контейнере, а вложенные формы, строки настроек или карточки наследуют ее геометрию. Меньше CSS-дублей - меньше риска, что после изменения breakpoint'а кнопки, поля и подписи начнут визуально разъезжаться.
Хорошие кандидаты:
• большие формы настроек
• checkout и billing flows
• dashboard-строки со сложной вложенностью
• карточки в дизайн-системе с общей action-зоной
Ограничение, о котором часто забывают
Недостаточно просто написать
Практический паттерн
Используйте
Fallback может быть обычной вертикальной формой. Интерфейс не ломается, а современные браузеры получают точное выравнивание.
Вывод:
В кабинетах, checkout, SaaS-формах и дизайн-системах часто нужно, чтобы вложенные секции попадали в колонки общего layout. Типичная ошибка - копировать
grid-template-columns в каждый компонент и потом ловить рассинхрон на адаптиве.Где помогает subgrid
subgrid позволяет вложенному grid использовать треки непосредственного родителя по колонкам или строкам. Это полезно, когда форма разбита на группы, но label, input и action-кнопки должны стоять по одной визуальной сетке..settings {
display: grid;
grid-template-columns: 160px minmax(0, 1fr) max-content;
gap: 12px 16px;
}
.group {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
gap: inherit;
}
.group > input {
grid-column: 2;
min-width: 0;
}Production-смысл
Так сетка описывается один раз в контейнере, а вложенные формы, строки настроек или карточки наследуют ее геометрию. Меньше CSS-дублей - меньше риска, что после изменения breakpoint'а кнопки, поля и подписи начнут визуально разъезжаться.
Хорошие кандидаты:
• большие формы настроек
• checkout и billing flows
• dashboard-строки со сложной вложенностью
• карточки в дизайн-системе с общей action-зоной
Ограничение, о котором часто забывают
subgrid работает только от непосредственного grid-контейнера, а сам элемент должен быть grid item и занимать нужный диапазон:.card {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}Недостаточно просто написать
grid-template-columns: subgrid внутри любого компонента - без родительской grid-сетки он не даст ожидаемого выравнивания.Практический паттерн
Используйте
subgrid там, где цена дублирования сетки реальна, и держите предсказуемый fallback:.form-group {
display: grid;
gap: 12px;
}
@supports (grid-template-columns: subgrid) {
.form-group {
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
}Fallback может быть обычной вертикальной формой. Интерфейс не ломается, а современные браузеры получают точное выравнивание.
Вывод:
subgrid стоит тащить в продакшен не ради синтаксиса, а там, где он снижает дублирование CSS и повышает стабильность сложного layout.❤2
100vh на мобильных все еще ломает full-screen интерфейсы: dvh/svh/lvh и safe-area для CTA без обрезания
В лендингах, paywall, onboarding, SaaS-кабинетах и дизайн-системах первый экран часто должен занять видимую высоту. Типичная ошибка - ставить
Единицы viewport
•
•
•
•
Production-паттерн
Trade-off
Практическое правило
• full-screen состояние -
• максимально стабильный первый экран -
• декоративный фон - можно
• нижний CTA - padding/bottom через
Вывод:
Мобильная верстка больше не сводится к
В лендингах, paywall, onboarding, SaaS-кабинетах и дизайн-системах первый экран часто должен занять видимую высоту. Типичная ошибка - ставить
height: 100vh и получать CTA под нижней панелью браузера или home indicator.Единицы viewport
•
100svh - высота при раскрытых панелях браузера: безопасно, но может дать запас.•
100lvh - высота при скрытых панелях: годится для фона, рискованна для контента.•
100dvh - текущая видимая высота: хороший выбор для экранов состояния.•
env(safe-area-inset-bottom) - защита CTA от системных зон.Production-паттерн
.screen{
min-height:100vh;
min-height:100svh;
min-height:100dvh;
display:flex;
flex-direction:column;
padding:max(16px,env(safe-area-inset-top)) 16px max(16px,env(safe-area-inset-bottom));
}
.screen__content{flex:1;min-height:0}
.screen__cta{position:sticky;bottom:max(16px,env(safe-area-inset-bottom))}Trade-off
dvh пересчитывается при изменении browser chrome и может давать визуальные скачки. Не вешайте height: 100dvh на длинные страницы: для hero, modal screen и onboarding чаще безопаснее min-height, чтобы контент мог вырасти.Практическое правило
• full-screen состояние -
min-height: 100dvh• максимально стабильный первый экран -
100svh• декоративный фон - можно
lvh• нижний CTA - padding/bottom через
max(..., env(safe-area-inset-bottom))Вывод:
Мобильная верстка больше не сводится к
100vh: выбирайте viewport-единицу под сценарий и всегда резервируйте safe-area.❤2👍1
field-sizing: content — авто-высота input и textarea без единой строчки JS
Раньше динамическую высоту полей ввода подстраивали через scrollHeight и обработчики событий. В production — формы обратной связи, комментарии в лендингах, кабинетах, SaaS-интерфейсах — это приводило к лишним рекалькуляциям и багам с resize. Частая ошибка — забывать сбрасывать замеры при скрытии полей или анимациях.
Как работает
Свойство применимо к
Production-применение
Поля обратной связи, комментарии, списки задач — везде, где контент непредсказуем. Особенно ценно в design systems и компонентных библиотеках, где высоту нельзя захардкодить. Вместо
Типичная ошибка
Полагаться только на
Trade-offs
JS всё ещё нужен для анимации расширения, поддержки старых браузеров (Firefox пока частично) или кастомных элементов. Но для типовых полей — это шаг к CSS как инструменту поведения, а не только оформления.
Вывод:
Инженерный takeaway —
Раньше динамическую высоту полей ввода подстраивали через scrollHeight и обработчики событий. В production — формы обратной связи, комментарии в лендингах, кабинетах, SaaS-интерфейсах — это приводило к лишним рекалькуляциям и багам с resize. Частая ошибка — забывать сбрасывать замеры при скрытии полей или анимациях.
Как работает
field-sizing: content заставляет браузер сам рассчитывать высоту по содержимому. Никакого JS, только CSS:textarea, input[type="text"] {
field-sizing: content;
min-height: 2em;
max-height: 10em;
}Свойство применимо к
textarea и текстовым input (text, email, search, tel, url, password). select не поддерживается. Важно: resize при этом не ломается, пользователь всё ещё может растягивать поле.Production-применение
Поля обратной связи, комментарии, списки задач — везде, где контент непредсказуем. Особенно ценно в design systems и компонентных библиотеках, где высоту нельзя захардкодить. Вместо
ResizeObserver и багов с размерами под скроллингом — предсказуемый layout.Типичная ошибка
Полагаться только на
field-sizing без ограничителей. Если контент длинный, поле заметно сдвинет соседние элементы. Всегда ставь min-height и max-height, чтобы контролировать стабильность интерфейса и избежать внезапного визуального регресса.Trade-offs
JS всё ещё нужен для анимации расширения, поддержки старых браузеров (Firefox пока частично) или кастомных элементов. Но для типовых полей — это шаг к CSS как инструменту поведения, а не только оформления.
Вывод:
Инженерный takeaway —
field-sizing: content убирает связку JS + scrollHeight, делая адаптивную высоту предсказуемой и поддерживаемой в любой сторонке.text-wrap: balance и text-box-trim: точный контроль многострочного текста без кастомных JS-обрезок и magic-отступов
Странное чувство, когда в Figma заголовок выглядит ровно, а в браузере последняя строка уходит в одну букву. Или когда карточка с текстом плывёт из-за того, что line-height даёт лишние отступы сверху и снизу. Это частая проблема в лендингах, кабинетах и дизайн-системах, где каждый пиксель важен для предсказуемости layout.
Раньше с этим воевали костылями. JS-скрипты, которые считают ширину строк и вставляют
Балансировка строк без JS
Удаление магических отступов
Вторая штука —
Trade-offs и поддержка
Минусы: balance не стоит вешать на всё подряд, только на короткие блоки. text-box-trim пока не завезли в стабильные браузеры. Но спецификация движется, и через пару лет это будет базой. Предупреждение: не используйте balance на hero-секциях с большим текстом — браузер может подтормаживать на мобильных.
Вывод:
Использование text-wrap: balance для заголовков и text-box-trim для точного выравнивания текста в layout-сетках — это шаг к стабильному и предсказуемому дизайну без хаков.
Странное чувство, когда в Figma заголовок выглядит ровно, а в браузере последняя строка уходит в одну букву. Или когда карточка с текстом плывёт из-за того, что line-height даёт лишние отступы сверху и снизу. Это частая проблема в лендингах, кабинетах и дизайн-системах, где каждый пиксель важен для предсказуемости layout.
Раньше с этим воевали костылями. JS-скрипты, которые считают ширину строк и вставляют
<br>. Или обнуление line-height с ручным подбором margin. В 2024 году можно проще.Балансировка строк без JS
text-wrap: balance берёт на себя балансировку строк. Браузер сам решает, где разорвать текст, чтобы все строки были примерно одинаковой длины. Особенно видно на заголовках из 2-4 строк. Классический пример — блок .title { text-wrap: balance; }. Но не кидайте это на параграфы. На длинных текстах браузер будет считать разрывы для каждой строки, и нагрузка растёт. Ошибка: вешать на все блоки подряд — теряете производительность на пересчёте.Удаление магических отступов
Вторая штука —
text-box-trim. Пока экспериментальная, работает в Safari и Chrome Canary. Убирает те самые «магические» отступы, которые line-height добавляет над первой строкой и под последней. Раньше приходилось выставлять отрицательный margin с дробными значениями. Теперь: text-box-trim: both; text-box-edge: cap alphabetic;. Полезно в карточках, кнопках, списках — сетка перестаёт плыть. Совет: тестируйте в production на проектах с поддержкой только современных браузеров.Trade-offs и поддержка
Минусы: balance не стоит вешать на всё подряд, только на короткие блоки. text-box-trim пока не завезли в стабильные браузеры. Но спецификация движется, и через пару лет это будет базой. Предупреждение: не используйте balance на hero-секциях с большим текстом — браузер может подтормаживать на мобильных.
Вывод:
Использование text-wrap: balance для заголовков и text-box-trim для точного выравнивания текста в layout-сетках — это шаг к стабильному и предсказуемому дизайну без хаков.
View Transitions API — SPA-подобные переходы без единой строчки JS
Раньше плавная навигация между страницами требовала сборку на SPA или извращения с fetch и ручной заменой DOM. View Transitions API ломает это правило — браузер сам снимает снэпшоты и анимирует переходы на обычном HTML/CSS. Частая ошибка: полагать, что MPA не может быть плавной, и тянуть тяжелый фреймворк для простого лендинга или статического сайта.
Базовый запуск — мета-тег
Просто добавьте в
Кастомные анимации через CSS
Переопределите псевдоэлементы
Практический совет: если анимация дергается, проверьте
Связывание элементов между страницами
Используйте
На странице товара — то же имя на блоке деталей. Браузер сопоставит их и анимирует трансформацию. Инженерный trade-off: это ломается, если имена не уникальны на странице или если элементы меняют DOM-позицию при ресайзе. Всегда тестируйте на адаптивных макетах.
Вывод:
View Transitions API — это инструмент для постепенного улучшения, который даёт нативную плавность MPA без оверхэда SPA, сохраняя семантику, доступность и простоту статической верстки.
Раньше плавная навигация между страницами требовала сборку на SPA или извращения с fetch и ручной заменой DOM. View Transitions API ломает это правило — браузер сам снимает снэпшоты и анимирует переходы на обычном HTML/CSS. Частая ошибка: полагать, что MPA не может быть плавной, и тянуть тяжелый фреймворк для простого лендинга или статического сайта.
Базовый запуск — мета-тег
Просто добавьте в
<head>: <meta name="view-transition" content="same-origin" />. Все переходы по ссылкам внутри домена получат дефолтную кросс-фейд анимацию. Без импортов, без сборки. Поддержка: Chrome 111+, Safari 18.2+. Firefox — в разработке, используйте как progressive enhancement.Кастомные анимации через CSS
Переопределите псевдоэлементы
::view-transition-old() и ::view-transition-new(). Production-пример: плавный скейл и сдвиг для списка статей на медиа-сайте:::view-transition-old(root) {
animation: fade-out 0.3s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
@keyframes fade-out {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.9); }
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}Практический совет: если анимация дергается, проверьте
contain: paint; на контейнере — это стабилизирует layout. Типичная ошибка: забыть про prefers-reduced-motion — браузер сам вырубает эффекты, но для кастомных анимаций лучше добавить обертку @media (prefers-reduced-motion: no-preference).Связывание элементов между страницами
Используйте
view-transition-name, чтобы анимировать конкретный компонент. Например, карточка товара на каталоге:.card { view-transition-name: product-card; }На странице товара — то же имя на блоке деталей. Браузер сопоставит их и анимирует трансформацию. Инженерный trade-off: это ломается, если имена не уникальны на странице или если элементы меняют DOM-позицию при ресайзе. Всегда тестируйте на адаптивных макетах.
Вывод:
View Transitions API — это инструмент для постепенного улучшения, который даёт нативную плавность MPA без оверхэда SPA, сохраняя семантику, доступность и простоту статической верстки.
❤1
CSS scroll-timeline: индикатор подписи внутри sticky без IntersectionObserver
Знакомый сценарий: sticky-блок с проверкой подписи. Внутри progress-бар, который должен заполняться, пока юзер скроллит секцию. Раньше — только IntersectionObserver или кастомные скролл-слушатели, которые дёргают layout. Теперь можно без JS на compositor-потоке.
Свойство
Как работает
Разбор:
Почему это удобно
* Анимация в compositor-потоке, без рывков на main thread.
* Работает внутри любого sticky: хедер, сайдбар, панель подписи.
* Не требует инициализации или подписки на DOM — предсказуемо с первого кадра.
Ограничения и trade-offs
Пока только Chromium (103+). Firefox за флагом
Production-кейсы: лендинги с длинным описанием подписи, страницы документации со sticky-оглавлением. Везде, где раньше ставил IntersectionObserver и вручную обновлял ширину. Предупреждение: не используй
Вывод:
Scroll-driven animations — это не хайп, а способ выкинуть JS из рендеринга для sticky-индикаторов, давая стабильный layout без дёрганий на мобильных устройствах.
Знакомый сценарий: sticky-блок с проверкой подписи. Внутри progress-бар, который должен заполняться, пока юзер скроллит секцию. Раньше — только IntersectionObserver или кастомные скролл-слушатели, которые дёргают layout. Теперь можно без JS на compositor-потоке.
Свойство
animation-timeline привязывает анимацию к прогрессу скролла родителя. Вместо времени — проскролленные пиксели. Для sticky это работает напрямую: блок висит, индикатор растёт, пока контент под ним прокручивается. Типичная ошибка — думать, что нужен JS для каждого пикселя прогресса.Как работает
@keyframes fill {
from { width: 0%; }
to { width: 100%; }
}
.sticky-indicator {
position: sticky;
top: 0;
height: 4px;
background: lightgray;
}
.sticky-indicator::after {
content: '';
display: block;
height: 100%;
background: #4caf50;
animation: fill 1s linear;
animation-timeline: scroll(block nearest);
}Разбор:
scroll(block nearest) привязывается к ближайшему скроллящемуся предку по блочной оси. ::after меняет ширину пропорционально скроллу. Никаких событий скролла и throttle.Почему это удобно
* Анимация в compositor-потоке, без рывков на main thread.
* Работает внутри любого sticky: хедер, сайдбар, панель подписи.
* Не требует инициализации или подписки на DOM — предсказуемо с первого кадра.
Ограничения и trade-offs
Пока только Chromium (103+). Firefox за флагом
layout.css.scroll-driven-animations.enabled. Safari в процессе. Если нужно несколько индикаторов на разных контейнерах — каждый получает свою timeline, что исключает гонки данных.Production-кейсы: лендинги с длинным описанием подписи, страницы документации со sticky-оглавлением. Везде, где раньше ставил IntersectionObserver и вручную обновлял ширину. Предупреждение: не используй
scroll(block nearest) без overflow-родителя — timeline не сработает, и анимация останется на старте.Вывод:
Scroll-driven animations — это не хайп, а способ выкинуть JS из рендеринга для sticky-индикаторов, давая стабильный layout без дёрганий на мобильных устройствах.
CSS light-dark(): адаптивная тёмная тема без дублирования цветов
В production-проектах — лендингах, админ-панелях, дизайн-системах — тёмная тема часто реализуется через
Как это работает
Функция
Ключевой момент:
Устранение дублирования переменных
Раньше приходилось писать два набора переменных под медиа-запросом. Теперь — один:
Типичная ошибка — забыть, что
Trade-offs и fallback
Поддержка: Chrome 123+, Safari 17.2+, Firefox 130+. Для старых браузеров обязателен fallback:
Советую внедрять в свежих проектах, где тёмная тема — полноценная схема, а не пара перекрашенных блоков. Код становится предсказуемее, а visual regression снижается.
Вывод:
В production-проектах — лендингах, админ-панелях, дизайн-системах — тёмная тема часто реализуется через
@media (prefers-color-scheme: dark) с полным копированием всех переменных. Одна правка в светлой — и приходится синхронизировать тёмную. Это ломает переиспользуемость и стабильность интерфейса.Как это работает
Функция
light-dark() принимает два аргумента: первый — цвет для светлой темы, второй — для тёмной. Но без color-scheme на :root она не знает контекста.:root {
color-scheme: light dark;
}
body {
background: light-dark(white, black);
color: light-dark(black, white);
}Ключевой момент:
color-scheme можно выставить вручную — например, color-scheme: dark — и функция вернёт тёмное значение вне зависимости от system preference. Это даёт гибкость для тестирования или ручного переключения.Устранение дублирования переменных
Раньше приходилось писать два набора переменных под медиа-запросом. Теперь — один:
:root {
--bg: light-dark(white, black);
--text: light-dark(black, white);
}Типичная ошибка — забыть, что
light-dark() работает только с цветами. url() или auto внутри не поддерживаются. Для градиентов — да, для ссылок — нет.Trade-offs и fallback
Поддержка: Chrome 123+, Safari 17.2+, Firefox 130+. Для старых браузеров обязателен fallback:
background: white;
background: light-dark(white, black);
Советую внедрять в свежих проектах, где тёмная тема — полноценная схема, а не пара перекрашенных блоков. Код становится предсказуемее, а visual regression снижается.
Вывод:
light-dark() избавляет от дублирования цветовых переменных в тёмной теме, но требует контроля color-scheme и fallback для старых браузеров.🔥2❤1
Overflow:clip и scrollbar-gutter: стабильные макеты без выпадения контента при появлении скроллбара
Замечали: страница грузится, контент уже почти встал, и тут появляется скроллбар — весь блок уезжает вправо на 16 пикселей. Или наоборот: открываешь модалку, body получает overflow:hidden, и макет дёргается. В production — от лендингов до админок — это ломает центровку, сдвигает заголовки и портит UX. Частая ошибка: ставят overflow:hidden и ждут стабильности, забывая, что он создаёт новый контекст скролла.
Overflow:clip
Свойство не резервирует место под скроллбар вообще. Контент не вылезет наружу, но и программный скроллинг не сработает. Годится для декоративных блоков, слайдеров, попапов — где скролла не будет никогда. Минимум сюрпризов, но не подходит для динамических списков.
Scrollbar-gutter: stable
Вот это уже ближе к повседневной верстке. Свойство говорит браузеру: оставь место под скроллбар, даже когда его нет. Ширина контента не прыгает ни при загрузке, ни при открытии модалки поверх:
Для симметрии (например, в карточках по центру) используйте
Trade-off и нюанс
-
-
- Но Safari (WebKit)
Вывод:
Связка
Замечали: страница грузится, контент уже почти встал, и тут появляется скроллбар — весь блок уезжает вправо на 16 пикселей. Или наоборот: открываешь модалку, body получает overflow:hidden, и макет дёргается. В production — от лендингов до админок — это ломает центровку, сдвигает заголовки и портит UX. Частая ошибка: ставят overflow:hidden и ждут стабильности, забывая, что он создаёт новый контекст скролла.
Overflow:clip
Свойство не резервирует место под скроллбар вообще. Контент не вылезет наружу, но и программный скроллинг не сработает. Годится для декоративных блоков, слайдеров, попапов — где скролла не будет никогда. Минимум сюрпризов, но не подходит для динамических списков.
Scrollbar-gutter: stable
Вот это уже ближе к повседневной верстке. Свойство говорит браузеру: оставь место под скроллбар, даже когда его нет. Ширина контента не прыгает ни при загрузке, ни при открытии модалки поверх:
.container {
overflow-y: auto;
scrollbar-gutter: stable;
}Для симметрии (например, в карточках по центру) используйте
stable both-edges.Trade-off и нюанс
-
clip — для элементов без скролла (слайдеры, декоративные врапперы).-
stable — для списков, таблиц, лент.- Но Safari (WebKit)
scrollbar-gutter пока не поддерживает. Придётся вручную резервировать ширину через padding-right под скроллбар — проверяйте поведение в DevTools на вкладке Rendering.Вывод:
Связка
overflow:clip и scrollbar-gutter:stable даёт предсказуемый layout без костылей для большей части браузеров, но требует fallback для WebKit.⚡1🔥1
Inertial scrolling и overscroll-behavior: управляем pull-to-refresh, bounce-эффектами и sticky-навигацией без js-костылей
На iOS при свайпе внутри модалки за ней летит весь сайт. На Android sticky-шапка выезжает за край экрана. Виноват inertial scrolling с bounce и pull-to-refresh. Раньше лечили
Как работает
Три значения:
Примеры в production
Фиксируем sticky-хедер, чтобы не выскальзывал:
Отключаем pull-to-refresh на модалке:
Изолируем всю страницу от bounce:
Почему это лучше JS
Производительность: не нужен
Типичная ошибка
На iOS при
Вывод:
На iOS при свайпе внутри модалки за ней летит весь сайт. На Android sticky-шапка выезжает за край экрана. Виноват inertial scrolling с bounce и pull-to-refresh. Раньше лечили
-webkit-overflow-scrolling и touchmove.preventDefault(). Сейчас есть overscroll-behavior — одно CSS-свойство без скриптов, решающее проблему в лендингах, кабинетах и дизайн-системах.Как работает
Три значения:
auto — поведение по умолчанию с bounce; contain — эффект внутри контейнера не выходит наружу; none — полное отключение bounce и pull-to-refresh. Важный нюанс — каскад: если у дочернего контейнера contain, родитель не получит лишнего скролла, даже когда дочерний уперся в границу.Примеры в production
Фиксируем sticky-хедер, чтобы не выскальзывал:
.sticky-header {
position: sticky;
top: 0;
overscroll-behavior: contain;
}Отключаем pull-to-refresh на модалке:
.modal-content {
overscroll-behavior: contain;
overflow-y: auto;
}Изолируем всю страницу от bounce:
html {
overscroll-behavior: none;
}Почему это лучше JS
Производительность: не нужен
touchmove с preventDefault(). Доступность: инерция внутри контейнера сохраняется, навигация снаружи не ломается. Подходит для SSR и статики — никаких скриптов.Типичная ошибка
На iOS при
overscroll-behavior: none bounce внутри элемента отключается. Если нужно сохранить инерцию внутри, но убрать на уровне страницы — ставьте contain на контейнере. Для sticky-элементов комбинируйте с contain на родителе, иначе навигация будет дергаться.Вывод:
overscroll-behavior — инженерный инструмент для модалок, дро-даунов, sticky-шапок и pull-to-refresh, избавляющий от JS-костылей и обеспечивающий стабильность layout.⚡1