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
Container Queries: Резиновые карточки товаров без медиазапросов по viewport
Карточки товаров, которые адаптируются к ширине родителя, а не экрана — это
Как это работает
Суть: вы пишете
Production-ready пример с grid
Родительская сетка:
А внутри самой карточки — контейнерные запросы:
Что это даёт. Карточка подстраивается внутри любого родителя — будь то колонка, сайдбар или flex-лента. Вам не нужно перебирать брейкпоинты под каждую секцию. Контент сам решает, когда менять раскладку.
Практический совет и предупреждение об ошибке
Типичная ошибка: заводить контейнер на body или глобальном wrapper. Это убивает смысл — запросы всё равно привязываются к viewport. Мой совет: заводите контейнер на уровне секций. Так проще контролировать вложенность и рендеринг быстрее.
Из trade-offs:
Вывод:
Container Queries делают компоненты по-настоящему автономными — вместо адаптации к экрану они адаптируются к контексту, что повышает стабильность интерфейса и упрощает поддержку дизайн-систем.
Карточки товаров, которые адаптируются к ширине родителя, а не экрана — это
@container. Эта техника незаменима в каталогах, сайдбарах и динамических сетках e-commerce, где одна и та же карточка может отображаться в колонке, группе или flex-ленте. Частая ошибка — пытаться подстроить компонент под разные контексты через медиазапросы по ширине экрана, что ведет к раздутым стилям и нестабильному layout.Как это работает
Суть: вы пишете
container-type: inline-size на родителе. И внутри карточки используете @container (max-width: 400px) { ... }. Стили сработают, когда сам контейнер сузится до указанной ширины, а не когда окно браузера станет меньше. Это дает предсказуемость: карточка адаптируется под своё непосредственное окружение.Production-ready пример с grid
Родительская сетка:
.cards-grid {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}А внутри самой карточки — контейнерные запросы:
.product-card {
display: flex;
flex-direction: column;
gap: 12px;
}
@container (min-width: 350px) {
.product-card {
flex-direction: row;
}
}
@container (max-width: 300px) {
.product-description {
display: none;
}
.product-price {
font-size: 14px;
}
}Что это даёт. Карточка подстраивается внутри любого родителя — будь то колонка, сайдбар или flex-лента. Вам не нужно перебирать брейкпоинты под каждую секцию. Контент сам решает, когда менять раскладку.
Практический совет и предупреждение об ошибке
Типичная ошибка: заводить контейнер на body или глобальном wrapper. Это убивает смысл — запросы всё равно привязываются к viewport. Мой совет: заводите контейнер на уровне секций. Так проще контролировать вложенность и рендеринг быстрее.
Из trade-offs:
@container работает в Chrome 105+ и Safari 16+. Для старых браузеров придётся вешать фолбэк на @media. И да, aspect-ratio внутри контейнера пока не совместим — баг редкий, но встречается в сложных layout-сетках.Вывод:
Container Queries делают компоненты по-настоящему автономными — вместо адаптации к экрану они адаптируются к контексту, что повышает стабильность интерфейса и упрощает поддержку дизайн-систем.
👍1
content-visibility: auto и contain-intrinsic-size: рендеринг только в видимой зоне без JS
Каталог на 500 товаров, лента блога или таблица лидеров — браузер честно считает layout и рисует каждый элемент, даже скрытый под скроллом. Результат: дёрганый скролл, высокий LCP и плавающий CLS. Многие идут в React Virtual, но для статичных списков есть решение на чистом CSS.
Как работают два свойства
Пример для карточки товара в каталоге:
Браузер на первом рендере отрисовывает только видимые карточки, а для скрытых резервирует 320x480px. Скролл стабильный, LCP снижается, layout не пересчитывается.
Где реально пригодится
Повторяющиеся карточки в e-commerce, лендинги с секциями, FAQ-аккордеоны, таблицы с десятками строк, бесконечные ленты без lazy-load. Для SSR и статичных страниц — идеально: никакой зависимость от JS.
Типичные ошибки и ограничения
* Не вешай на шапку сайта, первый экран или уникальные блоки — свойство только для повторяющихся элементов вне viewport.
* Анимации при появлении (Intersection Observer) могут не сработать — проверь отрисовку.
* Firefox поддерживает с 128 версии, Safari — частично. На production проверь через caniuse или используй как progressive enhancement.
Вывод:
Используйте
Каталог на 500 товаров, лента блога или таблица лидеров — браузер честно считает layout и рисует каждый элемент, даже скрытый под скроллом. Результат: дёрганый скролл, высокий LCP и плавающий CLS. Многие идут в React Virtual, но для статичных списков есть решение на чистом CSS.
Как работают два свойства
content-visibility: auto откладывает рендеринг, layout и paint элементов за пределами viewport. Браузер не тратит CPU на скрытые карточки. Но если не задать размер — полоса прокрутки сжимается, CLS растет. Решает contain-intrinsic-size: он резервирует место для скрытого блока, имитируя его размер.Пример для карточки товара в каталоге:
.product-card {
content-visibility: auto;
contain-intrinsic-size: 320px 480px;
}Браузер на первом рендере отрисовывает только видимые карточки, а для скрытых резервирует 320x480px. Скролл стабильный, LCP снижается, layout не пересчитывается.
Где реально пригодится
Повторяющиеся карточки в e-commerce, лендинги с секциями, FAQ-аккордеоны, таблицы с десятками строк, бесконечные ленты без lazy-load. Для SSR и статичных страниц — идеально: никакой зависимость от JS.
Типичные ошибки и ограничения
* Не вешай на шапку сайта, первый экран или уникальные блоки — свойство только для повторяющихся элементов вне viewport.
* Анимации при появлении (Intersection Observer) могут не сработать — проверь отрисовку.
* Firefox поддерживает с 128 версии, Safari — частично. На production проверь через caniuse или используй как progressive enhancement.
content-visibility: auto — не замена виртуализации с динамической подгрузкой. Но для статичных списков это бесплатный буст LCP и CLS без JS.Вывод:
Используйте
content-visibility: auto с contain-intrinsic-size для повторяющихся элементов — это даёт стабильный CLS и быстрый первый рендер без фреймворков.CSS anchor-positioning: тултипы с привязкой к контенту без ResizeObserver
Долгое время позиционирование тултипов и попапов в production было упражнением в ручных вычислениях: ResizeObserver, scroll-обработчики, проверки clientWidth и clientHeight, постоянное пересчитывание offset-ов при ресайзе. Особенно больно это в таблицах, где каждая ячейка может стать якорем, или в кастомных тултипах с адаптивными данными. CSS anchor-positioning переносит эту логику в браузер.
Механика: anchor-name и position-anchor
Элементу-якорю задаём
Тултип едет за якорем при скролле, подстраивается под viewport. Размеры можно привязать через
Где пригодится, а где нет
Подходит для tooltip, popover в таблицах, кастомных dropdown-меню с динамическим контентом. Отлично сочетается с
Не использовать, если проект на старых браузерах (поддержка: Chrome 125+, Safari 18+ с флагом, Firefox в разработке). Также не подходит для сложных кастомных анимаций —
Типичная ошибка и предупреждение
Ошибка: считать, что подойдёт для всех кейсов. Например, если внутри тултипа есть скроллящийся контент, при flip могут возникать баги с позиционированием. Обязательно тестируйте на реальных данных, особенно на мобильных устройствах. Ещё один trade-off — нет колбэков для кастомной логики, что важно для a11y: например, управление фокусом требует дописывания JS.
Вывод: anchor-positioning убирает ручной расчёт координат и makes адаптивные тултипы предсказуемыми, но не заменяет JS полностью — a11y и сложные анимации пока требуют гибридного подхода.
Долгое время позиционирование тултипов и попапов в production было упражнением в ручных вычислениях: ResizeObserver, scroll-обработчики, проверки clientWidth и clientHeight, постоянное пересчитывание offset-ов при ресайзе. Особенно больно это в таблицах, где каждая ячейка может стать якорем, или в кастомных тултипах с адаптивными данными. CSS anchor-positioning переносит эту логику в браузер.
Механика: anchor-name и position-anchor
Элементу-якорю задаём
anchor-name: --tooltip. Тултипу — position-anchor: --tooltip и inset-area для указания стороны появления. Если не хватает места, position-try-options: flip-block автоматически переворачивает блок без ручной проверки на overflow. Пример:.anchor {
anchor-name: --tooltip;
}
.tooltip {
position: fixed;
position-anchor: --tooltip;
inset-area: block-end;
position-try-options: flip-block;
}Тултип едет за якорем при скролле, подстраивается под viewport. Размеры можно привязать через
anchor-size(), например для ширины на весь блок.Где пригодится, а где нет
Подходит для tooltip, popover в таблицах, кастомных dropdown-меню с динамическим контентом. Отлично сочетается с
popover — браузер сам закрывает по клику вне и по Escape.Не использовать, если проект на старых браузерах (поддержка: Chrome 125+, Safari 18+ с флагом, Firefox в разработке). Также не подходит для сложных кастомных анимаций —
position-try-options не работает с transition, только через CSS animation.Типичная ошибка и предупреждение
Ошибка: считать, что подойдёт для всех кейсов. Например, если внутри тултипа есть скроллящийся контент, при flip могут возникать баги с позиционированием. Обязательно тестируйте на реальных данных, особенно на мобильных устройствах. Ещё один trade-off — нет колбэков для кастомной логики, что важно для a11y: например, управление фокусом требует дописывания JS.
Вывод: anchor-positioning убирает ручной расчёт координат и makes адаптивные тултипы предсказуемыми, но не заменяет JS полностью — a11y и сложные анимации пока требуют гибридного подхода.
👎1🔥1
Selectmenu с appearance: base-select: кастомные выпадающие списки без React Select или Select2
Кастомизация нативного select — вечная головная боль. JS-библиотеки тащат десятки килобайт кода и ломают доступность. Решение появилось в Chrome 133+:
Что даёт base-select
Пример production-решения для фильтра:
Практический совет: используйте
Ключевые ограничения и trade-offs
— Поддержка: только Chrome 133+ (в Firefox и Safari пока нет). Для production, где важна кросc-браузерность, — используйте как progressive enhancement.
— Не путать с
—
— Нельзя заменить option на произвольные HTML-элементы: это trade-off в пользу семантики и ARIA.
Предупреждение: не пытайтесь применять base-select для сложных компонентов с поиском или мультивыбором — для них всё ещё нужен JS. Типичная ошибка — стилизация
Когда использовать в production
Подходит для микро-интерфейсов: фильтры, формы обратной связи, кастомизация select в дизайн-системах. Если проект на чистом HTML/CSS — zero-dependency решение. Не подходит, если нужны Firefox/Safari или кастомная отрисовка option с иконками.
Вывод:
Кастомизация нативного select — вечная головная боль. JS-библиотеки тащат десятки килобайт кода и ломают доступность. Решение появилось в Chrome 133+:
appearance: base-select — это CSS-свойство, превращающее стандартный select в полностью стилизуемый компонент без единой строчки JavaScript. Главная ошибка — продолжать использовать appearance: none и вручную эмулировать поведение выпадашки.Что даёт base-select
appearance: base-select меняет модель отображения: option-элементы становятся стилизуемыми, появляются псевдоэлементы ::picker(select) (сам попап) и ::picker-icon (стрелка), а также псевдоклассы :open и :closed. В production-контексте — фильтры в админке, формы e-commerce, поиск по категориям — больше не нужен React Select или Select2.Пример production-решения для фильтра:
select {
appearance: base-select;
padding: 8px 12px;
border: 1px solid #c0c0c0;
border-radius: 4px;
}
select::picker(select) {
border: 1px solid #888;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-height: 200px;
}
option:hover {
background: #f0f0f0;
}
select::picker-icon {
content: '▼';
font-size: 10px;
color: #666;
}Практический совет: используйте
:open для стилей при открытом попапе — например, закругление рамки только сверху.Ключевые ограничения и trade-offs
— Поддержка: только Chrome 133+ (в Firefox и Safari пока нет). Для production, где важна кросc-браузерность, — используйте как progressive enhancement.
— Не путать с
appearance: none: base-select сохраняет нативное поведение и доступность.—
optgroup стилизуется через $lt;optgroup$gt; — жирный шрифт для подзаголовков.— Нельзя заменить option на произвольные HTML-элементы: это trade-off в пользу семантики и ARIA.
Предупреждение: не пытайтесь применять base-select для сложных компонентов с поиском или мультивыбором — для них всё ещё нужен JS. Типичная ошибка — стилизация
::picker(select) через position: absolute; это сломает выравнивание в layout.Когда использовать в production
Подходит для микро-интерфейсов: фильтры, формы обратной связи, кастомизация select в дизайн-системах. Если проект на чистом HTML/CSS — zero-dependency решение. Не подходит, если нужны Firefox/Safari или кастомная отрисовка option с иконками.
Вывод:
appearance: base-select — это сдвиг парадигмы: нативный select получает кастомизацию без JS, но с инженерным trade-off — только для Chromium, где стабильность layout и доступность важнее универсальной браузерной поддержки.CSS scroll-driven animations для параллакса и progress‑баров без ScrollTrigger
Scroll-driven анимации уже в Chrome и Safari — это не экспериментальная фича, а рабочий инструмент для production. Вместо подключения тяжёлого ScrollTrigger для простых эффектов вроде параллакса на hero‑секции или прогресс‑бара чтения статьи, хватит двух свойств. Частая ошибка — сразу тащить JS для анимаций, которые браузер может сделать сам, быстрее и без дёрганий.
Как работает animation‑timeline
Привяжи keyframes к прокрутке через
Progress‑бар и анимации при входе
Progress‑бар на чистом CSS: никаких вычислений, просто
Для анимаций при появлении —
Поддержка и fallback
Chrome 115+, Safari 18+, Firefox в процессе. Обязательно оборачивай в
Тогда в старых браузерах эффект исчезнет, но вёрстка не сломается. На iOS с
Trade‑offs
Минус: нельзя гибко контролировать скорость или easing, как в ScrollTrigger. Анимация строго пропорциональна прокрутке. Для сложных параллаксов с curve это не подходит. Плюс: нулевой JS, встроенная производительность, предсказуемый layout без перекомпоновки.
Вывод: Scroll‑driven анимации — production‑готовый инструмент для лёгких параллаксов и прогресс‑баров, который заменяет ScrollTrigger без потери производительности и с явным fallback через
Scroll-driven анимации уже в Chrome и Safari — это не экспериментальная фича, а рабочий инструмент для production. Вместо подключения тяжёлого ScrollTrigger для простых эффектов вроде параллакса на hero‑секции или прогресс‑бара чтения статьи, хватит двух свойств. Частая ошибка — сразу тащить JS для анимаций, которые браузер может сделать сам, быстрее и без дёрганий.
Как работает animation‑timeline
Привяжи keyframes к прокрутке через
scroll(). Браузер сам сопоставляет позицию скролла с ключевыми кадрами. Никаких слушателей scroll, никаких расчётов процентов. Пример — параллакс фона:.parallax-element {
animation: parallax linear;
animation-timeline: scroll(root block);
}
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-30%); }
}scroll(root block) — вертикальная ось, корневой контейнер. Если нужно ограничить скроллируемым блоком, используй nearest.Progress‑бар и анимации при входе
Progress‑бар на чистом CSS: никаких вычислений, просто
scaleX от 0 до 1..progress-bar {
height: 4px;
background: linear-gradient(90deg, #4facfe, #00f2fe);
transform-origin: 0 50%;
animation: grow linear;
animation-timeline: scroll(root block);
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}Для анимаций при появлении —
view(). Элемент плавно выезжает, когда допрокрутили до него:.reveal {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-in {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}animation‑range задаёт момент старта и конца (entry, exit, cover). Не злоупотребляй — каждая анимация на блоке может просадить FPS, несмотря на нативность.Поддержка и fallback
Chrome 115+, Safari 18+, Firefox в процессе. Обязательно оборачивай в
@supports:@supports (animation-timeline: scroll()) {
.parallax {
animation: parallax linear;
animation-timeline: scroll(root block);
}
}Тогда в старых браузерах эффект исчезнет, но вёрстка не сломается. На iOS с
position: fixed или внутри iframe возможны дёрганья — тестируй на реальных устройствах, а не только в DevTools.Trade‑offs
Минус: нельзя гибко контролировать скорость или easing, как в ScrollTrigger. Анимация строго пропорциональна прокрутке. Для сложных параллаксов с curve это не подходит. Плюс: нулевой JS, встроенная производительность, предсказуемый layout без перекомпоновки.
Вывод: Scroll‑driven анимации — production‑готовый инструмент для лёгких параллаксов и прогресс‑баров, который заменяет ScrollTrigger без потери производительности и с явным fallback через
@supports.👍1
@starting-style и transition-behavior: allow-discrete: анимация появления display: none элементов без JS-классов и requestAnimationFrame
Раньше плавное появление скрытого элемента требовало костылей: setTimeout, requestAnimationFrame, манипуляции с visibility и высотой. В production-интерфейсах — модалки, дропдауны, тултипы — этот паттерн выглядел как хрупкая конструкция, ломающаяся при малейшей асинхронной загрузке. Типичная ошибка: пытаться анимировать display напрямую, но CSS игнорирует переходы для дискретных свойств.
Как это работает
Добавляем класс
Production-ориентированный пример
В дизайн-системах для адаптивных лендингов используйте этот подход для аккордеонов или показа дополнительных опций. Но помните: обратный переход при скрытии тоже анимируется, если прописать
Когда не стоит полагаться
Типичная ошибка: пытаться анимировать
Практический совет
Для компонентов, которые часто появляются и исчезают (тултипы, выпадающие списки), проверьте кросс-браузерность: добавьте fallback через JS с помощью
Вывод:
Раньше плавное появление скрытого элемента требовало костылей: setTimeout, requestAnimationFrame, манипуляции с visibility и высотой. В production-интерфейсах — модалки, дропдауны, тултипы — этот паттерн выглядел как хрупкая конструкция, ломающаяся при малейшей асинхронной загрузке. Типичная ошибка: пытаться анимировать display напрямую, но CSS игнорирует переходы для дискретных свойств.
Как это работает
@starting-style задаёт начальные стили для только что добавленного элемента. А transition-behavior: allow-discrete разрешает анимировать дискретные свойства, например display. В сочетании с обычным transition браузер сам вычисляет промежуточные состояния. Пример для модалки:.modal {
display: none;
opacity: 0;
transition: opacity 0.5s, display 0.5s allow-discrete;
}
.modal.show {
display: block;
opacity: 1;
}
@starting-style {
.modal.show {
opacity: 0;
}
}Добавляем класс
show — элемент плавно появляется. Без JS-триггеров и магии. Браузер сам оптимизирует анимацию, не блокируя main thread.Production-ориентированный пример
В дизайн-системах для адаптивных лендингов используйте этот подход для аккордеонов или показа дополнительных опций. Но помните: обратный переход при скрытии тоже анимируется, если прописать
@starting-style для состояния без класса. Проверяйте поддержку: Chrome 117+, Edge, Opera. Firefox и Safari пока не имплементировали.Когда не стоит полагаться
Типичная ошибка: пытаться анимировать
display в сочетании с большими макетами, где браузер может отказаться от рефлоу. Для сложных виджетов (например, с изменением grid-размеров) всё ещё надёжнее использовать requestAnimationFrame или content-visibility. Также помните, что @starting-style требует явного класса или состояния — для псевдоклассов вроде :hover это не сработает.Практический совет
Для компонентов, которые часто появляются и исчезают (тултипы, выпадающие списки), проверьте кросс-браузерность: добавьте fallback через JS с помощью
Web Animations API на случай отсутствия поддержки. Это сохранит UX без потери производительности.Вывод:
@starting-style с allow-discrete — лаконичный паттерн для простых сценариев, но для production-уровня требуются fallback и тестирование на Firefox и Safari.👍1
Наконец-то нормальная анимация
Раньше, чтобы плавно раскрыть элемент от
Как работает
На родителе или
Это свойство позволяет CSS интерполировать значения
Реальный пример — аккордеон
Свойство
Второй аргумент
Почему это круто
- Больше не нужно угадывать
- Никакого JS для измерения
- Работает для
Типичная ошибка
Многие разработчики забывают про
Вывод:
height: auto без костылейРаньше, чтобы плавно раскрыть элемент от
0 до auto, приходилось идти на ухищрения: угадывать max-height: 9999px с дергающейся анимацией, измерять scrollHeight через JS или городить grid-конструкции с 0fr в 1fr. Все это — хрупкие решения, которые ломаются при изменении контента или на разных устройствах. Теперь в CSS есть встроенная поддержка для этой задачи.Как работает
interpolate-sizeНа родителе или
:root включаем разрешение анимации ключевых слов::root {
interpolate-size: allow-keywords;
}Это свойство позволяет CSS интерполировать значения
auto, min-content, max-content и fit-content в анимациях. Без него браузер не знает, как плавно перейти от 0 к auto.Реальный пример — аккордеон
Свойство
calc-size() задает конечное положение:.panel {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.panel.active {
height: calc-size(auto, size);
}Второй аргумент
size — это псевдофункция, которая возвращает окончательное вычисленное значение. Анимация идет от 0 до этого значения плавно, без рывков.Почему это круто
- Больше не нужно угадывать
max-height: 9999px — анимация точная под любой контент.- Никакого JS для измерения
scrollHeight — уходит лишняя перерисовка и зависимость от событий.- Работает для
height, width, flex-basis — подходит для дропдаунов, аккордеонов, модалок с изменяемой высотой.Типичная ошибка
Многие разработчики забывают про
interpolate-size и пытаются анимировать calc-size(auto) без него. Это не сработает, и элемент просто прыгнет на финальную высоту. Обязательно добавьте interpolate-size: allow-keywords на родителя или :root.Вывод:
interpolate-size и calc-size() решают давнюю проблему плавного раскрытия контента, избавляя от устаревших хаков и делая анимации предсказуемыми на production — но пока только в Chrome 129+.👍3❤1
Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли.
Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM.
Please open Telegram to view this post
VIEW IN TELEGRAM
field-sizing: content — как убить JS-костыли для авто-высоты textarea и input
Раньше, чтобы поле ввода росло динамически, приходилось тащить скрипты, высчитывать scrollHeight или плясать с contenteditable. В production — лендинги, кабинеты, формы в дизайн-системах — это всегда лишний код. Теперь хватит одной строки CSS.
Как это работает
Для textarea:
Поле само расширяется вниз. Для input — по ширине:
Без свойства элемент фиксированного размера. С ним — подстраивается под содержимое.
Нюансы и типичная ошибка
Ключевой момент:
Свойство дружит с
Production-пример для форм
Браузерная поддержка: Chrome 123+ и Edge — да (работает в продакшене). Safari пока в Technology Preview, Firefox — в Nightly под флагом. Используйте как прогрессивное улучшение: старые браузеры увидят фиксированный размер — не критично.
Вывод:
Одна строка CSS вместо целого скрипта — чище код, быстрее загрузка, меньше багов в формах.
Раньше, чтобы поле ввода росло динамически, приходилось тащить скрипты, высчитывать scrollHeight или плясать с contenteditable. В production — лендинги, кабинеты, формы в дизайн-системах — это всегда лишний код. Теперь хватит одной строки CSS.
Как это работает
Для textarea:
textarea {
field-sizing: content;
min-height: 3em;
width: 100%;
}Поле само расширяется вниз. Для input — по ширине:
input[type="text"] {
field-sizing: content;
min-width: 150px;
max-width: 500px;
}Без свойства элемент фиксированного размера. С ним — подстраивается под содержимое.
Нюансы и типичная ошибка
Ключевой момент:
field-sizing сбрасывает height и width в auto. Если задать height: 50px, это не сработает. Ставьте только min-height и max-height, иначе форма схлопнется по контенту.Свойство дружит с
resize: vertical — пользователь может вручную тянуть высоту, автомасштабирование не ломается.Production-пример для форм
.auto-textarea {
field-sizing: content;
min-height: 2.5em;
max-height: 15em;
width: 100%;
resize: vertical;
overflow-y: auto;
}Браузерная поддержка: Chrome 123+ и Edge — да (работает в продакшене). Safari пока в Technology Preview, Firefox — в Nightly под флагом. Используйте как прогрессивное улучшение: старые браузеры увидят фиксированный размер — не критично.
Вывод:
Одна строка CSS вместо целого скрипта — чище код, быстрее загрузка, меньше багов в формах.
👍2
Cascade Layers без specificity-войн:
На реальном проекте с тысячами строк CSS проблема не в адаптивности, а в том, что каждый второй селектор —
Схема миграции
Сначала объявляешь слой для старых стилей:
Production-подключение через
Современные браузеры грузят новый CSS через
А для старых (IE11, древний Safari) — обычный
Типичная ошибка и trade-off
Ошибка — пытаться засунуть в
Вывод:
Связка
@import с supports()На реальном проекте с тысячами строк CSS проблема не в адаптивности, а в том, что каждый второй селектор —
#page .wrapper .card .title a, а !important разбросан, как конфетти. Специфичность растёт быстрее, чем число страниц. Выход — Cascade Layers, но на legacy-коде переписать всё под @layer нельзя. Помогает связка @import с supports().Схема миграции
Сначала объявляешь слой для старых стилей:
@layer legacy;. Всё, что туда попадёт, получает самый низкий приоритет. Даже #header .card .title из legacy не перекроет .card из более высокого слоя. Затем задаёшь иерархию для новых: @layer base, components, utilities;. Теперь есть чёткая линейка — компоненты перебивают базу, утилиты перебивают компоненты. Без вложенности в 5 уровней.Production-подключение через
@importСовременные браузеры грузят новый CSS через
@import с supports() и сразу кладут его в нужный слой:@import url("new.css") supports(display: grid) layer(components);А для старых (IE11, древний Safari) — обычный
@import на legacy файл. Они проигнорируют supports и layer, получат старые стили и не сломаются. Это даёт cross-browser fallback без хаков.Типичная ошибка и trade-off
Ошибка — пытаться засунуть в
@layer всё сразу. На деле слой legacy должен быть объявлен до всех @import. А лучше постепенно вытаскивать компоненты в свои слои. Это про распределение приоритетов, а не про переписывание кода с нуля. Из нюансов: старые браузеры, не поддерживающие @layer (например, IE11), увидят только обычные стили, поэтому важно, чтобы legacy-файл не переопределял новые компоненты через высокую специфичность.Вывод:
Связка
@import supports(...) layer() позволяет внедрить Cascade Layers на legacy-проекте без слома старых стилей и specificity-войн, сохраняя предсказуемость layout..card:has(.card__button:focus) — селектор, который выкидывает BEM-костыли на свалку
Помните ту боль, когда для тени на карточке из-за фокуса кнопки приходилось лезть в HTML и добавлять родителю модификатор? BEM-пуристы хватались за голову, а разработчики плодили классы в JS или натыкали pointer-events.
Проблема: состояние потомка -> стиль родителя
Типичный production-кейс: карточка товара в e-commerce или лендинге. Внутри кнопка "Купить" в фокусе. Нужно визуально подсветить всю карточку, например тенью. Без
Реальный кейс: формы с валидацией
Есть список
Это работает без JS, не трогает HTML и делает стили зависимыми от реального состояния DOM, а не от модификаторов.
Где :has() реально спасает в production
* Дропдауны и аккордеоны — стили родителя меняются при открытии вложенного состояния.
* Карточки с разным контентом — разный паддинг или border в зависимости от вложенного блока (например,
* Формы с ошибками — родитель подкрашивается сам, когда инпут в invalid или focus.
Типичная ошибка: забыть про специфичность
Вывод:
Помните ту боль, когда для тени на карточке из-за фокуса кнопки приходилось лезть в HTML и добавлять родителю модификатор? BEM-пуристы хватались за голову, а разработчики плодили классы в JS или натыкали pointer-events.
:has() решает это без единого хака и без изменения разметки.Проблема: состояние потомка -> стиль родителя
Типичный production-кейс: карточка товара в e-commerce или лендинге. Внутри кнопка "Купить" в фокусе. Нужно визуально подсветить всю карточку, например тенью. Без
:has() — только JS-навешивание класса на родителя или BEM-модификатор, который загрязняет HTML. :has() делает это строго в CSS:.card:has(.card__button:focus) {
box-shadow: 0 0 0 4px blue;
}
Реальный кейс: формы с валидацией
Есть список
<ul>, внутри <li> с инпутом, у которого aria-invalid="true". Нужно выделить весь <li> красной рамкой без дополнительных классов. Решение:li:has(input[aria-invalid="true"]) {
border: 2px solid red;
background: #ffe0e0;
}
Это работает без JS, не трогает HTML и делает стили зависимыми от реального состояния DOM, а не от модификаторов.
Где :has() реально спасает в production
* Дропдауны и аккордеоны — стили родителя меняются при открытии вложенного состояния.
* Карточки с разным контентом — разный паддинг или border в зависимости от вложенного блока (например,
.card:has(> .image)).* Формы с ошибками — родитель подкрашивается сам, когда инпут в invalid или focus.
Типичная ошибка: забыть про специфичность
:has() — псевдокласс, он не увеличивает специфичность родителя. Это значит, селектор .card:has(.button) имеет ту же весомость, что и .card. Не комбинируйте его с другими псевдоклассами бездумно. И помните: Edge до 89 и Opera Mini не поддерживают :has(). Для критичных интерфейсов — делайте fallback, например, через CSS-хак с @supports или базовый стиль без :has().Вывод:
:has() переводит CSS с уровня "реакция на классы" на уровень "реакция на реальное состояние DOM", уменьшая связанность HTML и стилей, и избавляет от BEM-костылей в production-верстке.👍3
CSS light-dark() и color-mix(): темизация без CSS-переменных под каждый режим
Многие верстальщики по привычке заводят десятки переменных в
light-dark() — одна запись вместо двух наборов переменных
Вместо того чтобы писать:
Достаточно задать:
Функция сама определяет активную тему через
Ошибка: если не задать
Меняешь
color-mix() — адаптивные оттенки без препроцессоров
Если нужно смешивать цвета,
Результат — оттенок акцента, осветлённый на белом фоне и затемнённый на чёрном. Без лишних переменных и JS-расчётов.
Подводный камень:
Вывод:
Многие верстальщики по привычке заводят десятки переменных в
:root для светлой темы и дублируют их в [data-theme="dark"]. Это не только раздувает код, но и создаёт риск рассинхронизации значений при рефакторинге. Современные CSS-функции light-dark() и color-mix() позволяют избавиться от этой практики, перенося логику на уровень браузера.light-dark() — одна запись вместо двух наборов переменных
Вместо того чтобы писать:
:root { --bg: white; --text: black; }
[data-theme="dark"] { --bg: black; --text: white; }Достаточно задать:
:root { color-scheme: light dark; }
body {
background: light-dark(white, black);
color: light-dark(black, white);
}Функция сама определяет активную тему через
color-scheme. Первый аргумент — цвет для светлой, второй — для тёмной. Никаких media-запросов, никакого дубляжа правил. Это работает в production: лендинги, админки, дизайн-системы — везде, где тема переключается системно или вручную.Ошибка: если не задать
color-scheme на элементе или его родителе, light-dark() просто не сработает. Браузер не поймёт, какой режим считать светлым. Для ручного переключения через атрибут пиши:[data-theme="dark"] { color-scheme: dark; }
[data-theme="light"] { color-scheme: light; }Меняешь
data-theme через JS — функция подхватывает контекст.color-mix() — адаптивные оттенки без препроцессоров
Если нужно смешивать цвета,
color-mix() даёт точную колориметрию на уровне CSS. Например, сделать полупрозрачный акцент, который адаптируется под фон::root { --accent: #007bff; }
.button {
background: color-mix(in srgb, var(--accent) 70%, light-dark(white, black));
}Результат — оттенок акцента, осветлённый на белом фоне и затемнённый на чёрном. Без лишних переменных и JS-расчётов.
Подводный камень:
color-mix() требует указать цветовое пространство (например, in srgb). Без этого оно не сработает. Также смешивание в sRGB может давать менее насыщенные цвета по сравнению с OKLCH — это trade-off между предсказуемостью и точностью.Вывод:
light-dark() и color-mix() — это не синтаксический сахар, а инженерный инструмент, который сокращает объём CSS, повышает согласованность тем и устраняет ручное дублирование.👍1
Please open Telegram to view this post
VIEW IN TELEGRAM
CSS scroll-timeline и view-timeline: анимации по прокрутке без тонны JS и Intersection Observer
Раньше синхронизация анимаций с прокруткой требовала Intersection Observer или ручного расчёта прогресса через событие scroll — это приводило к лишним пересчётам layout, дерганью на мобильных устройствах и портянке кода в production-проектах: от лендингов до дизайн-систем и e-commerce интерфейсов. Частая ошибка верстальщиков — городить JS-решения для базовых эффектов, которые браузер давно может сделать сам.
Как работает scroll-timeline
Привязывает анимацию к прогрессу прокрутки контейнера. Например, прогресс-бар чтения статьи заполняется по мере скролла страницы — без единого обработчика.
Как работает view-timeline
Анимация привязана к видимости элемента во вьюпорте. Идеально для плавных появлений при скролле — например, для карточек в сетке.
Практический совет: используй
Почему это круто
Никакого JS — ни
Ограничения
Поддержка пока в Chrome 115+, Edge 115+ и Safari Technology Preview. В Firefox — за флагом
Вывод: scroll-timeline и view-timeline — готовый production-инструмент для современных проектов, который экономит время и повышает стабильность интерфейса за счёт отказа от JS-оверхеда.
Раньше синхронизация анимаций с прокруткой требовала Intersection Observer или ручного расчёта прогресса через событие scroll — это приводило к лишним пересчётам layout, дерганью на мобильных устройствах и портянке кода в production-проектах: от лендингов до дизайн-систем и e-commerce интерфейсов. Частая ошибка верстальщиков — городить JS-решения для базовых эффектов, которые браузер давно может сделать сам.
Как работает scroll-timeline
Привязывает анимацию к прогрессу прокрутки контейнера. Например, прогресс-бар чтения статьи заполняется по мере скролла страницы — без единого обработчика.
@keyframes grow {
from { width: 0%; }
to { width: 100%; }
}
.scroll-container {
scroll-timeline: --scroll-progress;
overflow-y: scroll;
height: 200vh;
}
.progress-bar {
animation: grow 1s linear;
animation-timeline: --scroll-progress;
}Как работает view-timeline
Анимация привязана к видимости элемента во вьюпорте. Идеально для плавных появлений при скролле — например, для карточек в сетке.
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.element {
view-timeline: --element-view;
animation: fadeInUp 1s linear;
animation-timeline: --element-view;
}Практический совет: используй
animation-fill-mode: backwards или начальное состояние в CSS, чтобы элемент не отображался до начала анимации. Типичная ошибка — забыть задать overflow-y: scroll для контейнера с scroll-timeline; без этого анимация не сработает.Почему это круто
Никакого JS — ни
requestAnimationFrame, ни обработчиков scroll. Браузер обрабатывает анимации на композитном потоке, без пересчёта DOM и лишних repaint. Гибко: можно комбинировать с обычными временными анимациями через animation-timeline: auto или задавать свои шкалы.Ограничения
Поддержка пока в Chrome 115+, Edge 115+ и Safari Technology Preview. В Firefox — за флагом
layout.css.scroll-anchoring.enabled. Для сложных сценариев с несколькими анимациями и разными триггерами потребуется больше планирования. Trade-off: если проект требует поддержки старых браузеров, придётся использовать fallback на JS.Вывод: scroll-timeline и view-timeline — готовый production-инструмент для современных проектов, который экономит время и повышает стабильность интерфейса за счёт отказа от JS-оверхеда.
👍1
text-box-trim и text-box-edge: теперь не нужно гадать, где заканчивается шрифт
Выравнивание текста по вертикали — вечная головная боль. Ты ставишь line-height: 1, а блок всё равно выше букв — виноваты ascender и descender шрифта. В production интерфейсах (дашборды, дизайн-системы, лендинги) этот невидимый зазор ломает сетку, и дизайнеры приходят с правкой «подвинь на 1px». Ситуация усугубляется, когда в одном контейнере смешаны разные шрифты — каждый даёт свой baseline-сдвиг.
Как это работает
Свойства text-box-trim и text-box-edge режут лишнее пространство вокруг глифов. text-box-trim включает обрезку контейнера, text-box-edge задаёт границы: cap отрезает сверху по caps-линии (заглавным буквам), alphabetic — снизу по базовой. Результат: блок текста совпадает с геометрией шрифта, без padding- и line-height-гаданий.
Почему это меняет подход к типографике
Во-первых, исчезает необходимость в дробных line-height и магических числах для baseline-сдвига — контейнер сам подстраивается под шрифт. Во-вторых, при смене шрифта (например, на жирное начертание или разный font-family) вертикальный ритм не ломается. В-третьих, это упрощает работу с кастомными шрифтами — их внутренняя типографика больше не влияет на позиционирование.
Типичная ошибка и практический совет
Ошибка: считать, что text-box-trim заменяет line-height. На самом деле он обрезает внешний контейнер, но не меняет интерлиньяж. Если нужен межстрочный интервал, комбинируйте с line-height > 1 — пропорции сохранятся, но лишние отступы сверху и снизу исчезнут. Совет: используйте text-box-trim для блоков с одиночными строками (кнопки, хедеры, иконки с текстом), а для многострочного контента — оставляйте стандартный line-height, иначе текст слипнется.
Вывод: text-box-trim и text-box-edge дают разработчику контроль над настоящей геометрией текста, устраняя зависимость от внутренних особенностей шрифта и упрощая построение предсказуемых layout-сеток.
Выравнивание текста по вертикали — вечная головная боль. Ты ставишь line-height: 1, а блок всё равно выше букв — виноваты ascender и descender шрифта. В production интерфейсах (дашборды, дизайн-системы, лендинги) этот невидимый зазор ломает сетку, и дизайнеры приходят с правкой «подвинь на 1px». Ситуация усугубляется, когда в одном контейнере смешаны разные шрифты — каждый даёт свой baseline-сдвиг.
Как это работает
Свойства text-box-trim и text-box-edge режут лишнее пространство вокруг глифов. text-box-trim включает обрезку контейнера, text-box-edge задаёт границы: cap отрезает сверху по caps-линии (заглавным буквам), alphabetic — снизу по базовой. Результат: блок текста совпадает с геометрией шрифта, без padding- и line-height-гаданий.
.title {
font-size: 40px;
line-height: 1;
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
}Почему это меняет подход к типографике
Во-первых, исчезает необходимость в дробных line-height и магических числах для baseline-сдвига — контейнер сам подстраивается под шрифт. Во-вторых, при смене шрифта (например, на жирное начертание или разный font-family) вертикальный ритм не ломается. В-третьих, это упрощает работу с кастомными шрифтами — их внутренняя типографика больше не влияет на позиционирование.
Типичная ошибка и практический совет
Ошибка: считать, что text-box-trim заменяет line-height. На самом деле он обрезает внешний контейнер, но не меняет интерлиньяж. Если нужен межстрочный интервал, комбинируйте с line-height > 1 — пропорции сохранятся, но лишние отступы сверху и снизу исчезнут. Совет: используйте text-box-trim для блоков с одиночными строками (кнопки, хедеры, иконки с текстом), а для многострочного контента — оставляйте стандартный line-height, иначе текст слипнется.
Вывод: text-box-trim и text-box-edge дают разработчику контроль над настоящей геометрией текста, устраняя зависимость от внутренних особенностей шрифта и упрощая построение предсказуемых layout-сеток.
position: sticky и overflow: hidden: хрупкий союз, который ломает верстку
Когда у родителя стоит overflow: hidden, sticky-элемент упирается в его границы и не работает. Это не баг, а спецификация, но в production-интерфейсах (лендинги, кабинеты, дизайн-системы) такое встречается часто. Ошибка — думать, что обход только через overflow: visible или auto.
Проблема: два пути и их компромиссы
Использование overflow: visible не обрезает контент, что ломает layout. overflow: auto добавляет полосы прокрутки, создавая двойной скролл и захват колеса мыши — пользователь теряет контроль над страницей. Оба варианта нарушают стабильность интерфейса.
Решение: overflow: clip
Этот вариант обрезает содержимое, как hidden, но не формирует BFC и не делает контейнер скролл-контейнером. Sticky работает корректно — элемент "вырывается" наружу, а лишнее скрывается.
Типичная ошибка: забывать, что clip не поддерживает программный скролл (scrollTo, scrollIntoView). Если контейнер скроллится сам, clip не подходит. Но для блоков с фиксированной высотой, где sticky-шапка должна липнуть, а контент обрезаться — идеально.
Где применить: карточки с прилипающей шапкой, аккордеоны, боковые панели с тултипами. Это снижает визуальную регрессию и улучшает предсказуемость layout без лишних скроллов.
Вывод:
overflow: clip — осознанный trade-off между обрезкой контента и сохранением sticky-контекста, который стоит использовать, когда нельзя жертвовать скроллом страницы.
Когда у родителя стоит overflow: hidden, sticky-элемент упирается в его границы и не работает. Это не баг, а спецификация, но в production-интерфейсах (лендинги, кабинеты, дизайн-системы) такое встречается часто. Ошибка — думать, что обход только через overflow: visible или auto.
Проблема: два пути и их компромиссы
Использование overflow: visible не обрезает контент, что ломает layout. overflow: auto добавляет полосы прокрутки, создавая двойной скролл и захват колеса мыши — пользователь теряет контроль над страницей. Оба варианта нарушают стабильность интерфейса.
Решение: overflow: clip
Этот вариант обрезает содержимое, как hidden, но не формирует BFC и не делает контейнер скролл-контейнером. Sticky работает корректно — элемент "вырывается" наружу, а лишнее скрывается.
.card-list {
overflow: clip;
position: relative;
max-height: 500px;
}
.card-header {
position: sticky;
top: 0;
z-index: 1;
}Типичная ошибка: забывать, что clip не поддерживает программный скролл (scrollTo, scrollIntoView). Если контейнер скроллится сам, clip не подходит. Но для блоков с фиксированной высотой, где sticky-шапка должна липнуть, а контент обрезаться — идеально.
Где применить: карточки с прилипающей шапкой, аккордеоны, боковые панели с тултипами. Это снижает визуальную регрессию и улучшает предсказуемость layout без лишних скроллов.
Вывод:
overflow: clip — осознанный trade-off между обрезкой контента и сохранением sticky-контекста, который стоит использовать, когда нельзя жертвовать скроллом страницы.
👍3❤1