es
Feedback
Логово верстальщика

Логово верстальщика

Ir al canal en Telegram

Логово верстальщиков: HTML, CSS, JavaScript, практики современной верстки, вайбкодинг и использование ИИ в разработке. Личный блог автора - @just_genych По вопросам рекламы или разработки: @g_abashkin

Mostrar más
8 194
Suscriptores
+224 horas
-157 días
-9230 días
Archivo de publicaciones
Cascade Layers без specificity-войн: @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.

field-sizing: content — как убить JS-костыли для авто-высоты textarea и input Раньше, чтобы поле ввода росло динамически, приходилось тащить скрипты, высчитывать 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 вместо целого скрипта — чище код, быстрее загрузка, меньше багов в формах.

🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собес
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли. Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM. ✖️ xCode Journal

Наконец-то нормальная анимация 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+.

@starting-style и transition-behavior: allow-discrete: анимация появления display: none элементов без JS-классов и requestAnimationFrame Раньше плавное появление скрытого элемента требовало костылей: 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.

После проверки на доступность иногда ломается представление о нормальном интерфейсе С виду всё работает: кнопки нажимаются, форма отправляется, модалка открывается, табы переключаются. А потом интерфейс попадает к пользователям и выясняется: ❌ кнопка “закрыть” читается как “button”; ❌ фокус не попал в модалку; ❌ ошибка в форме есть визуально, но человек с дальтонизмом её не увидит; ❌ кастомный селект выглядит красиво, но с клавиатуры почти непроходим. И это не проблема пользователя, а баги продукта. Для тех, кто хочет разобраться в цифровой доступности системно, 4 июля стартует Accessibility Unity — онлайн-школа для разработчиков, тестировщиков, дизайнеров и исследователей. Курс идёт 10 недель. Участники сначала разбираются в потребностях людей с инвалидностью, потом изучают теорию, специальные возможности и нестандартные интерфейсы, а затем переходят к практике, тестированию и защите кейса. Лекторы — практики из Яндекса, Сбера, Dodo Engineering и Jazari One. Отдельный плюс — на курсе есть лекторы и тестировщики с инвалидностью. Это помогает быстро снять иллюзию, что доступность заключается в паре чекбоксов в конце. На деле она начинается с решений в дизайне, семантике, навигации, состояниях, фокусе и тестировании. Формат онлайн: вебинары, записи, консультации, тесты и фидбек. Старт — 4 июля, окончание — 29 августа. Подробности и запись тут По всем вопросам сюда

CSS scroll-driven animations для параллакса и progress‑баров без ScrollTrigger 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.

Selectmenu с appearance: base-select: кастомные выпадающие списки без React Select или Select2 Кастомизация нативного 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 и доступность важнее универсальной браузерной поддержки.

ИИ vs ЧЕЛОВЕК / AI УЖЕ МНОГОЕ УМЕЕТ, НО НЕ ТАК КАК ТЫ ... Нейросети уже пишут, рисуют и отвечают 24/7. Это мощно, и мы за про
ИИ vs ЧЕЛОВЕК / AI УЖЕ МНОГОЕ УМЕЕТ, НО НЕ ТАК КАК ТЫ ... Нейросети уже пишут, рисуют и отвечают 24/7. Это мощно, и мы за прогресс. Но есть вещи, которые алгоритмы никогда не заменят: — эмпатию к клиенту — доверие, которое строится годами — продажи без манипуляций, с душой ⚠️ Технологии — это инструмент, а главное — это ты и твой живой контакт. Приглашаем тебя в ЭКО-Пространство, где технологии — это фон, а главное — это ты и твой клиент ✔️ В этой ПОДБОРКЕ есть кое-что поважнее алгоритмов — ДОВЕРИЕ. В папке собраны каналы про экологичные продажи, про понимание, про рост без выгорания. Пусть ИИ пишет тексты, а ты учись создавать отношения. 💚 Добавляй папку в свой актив и делись с друзьями! 📌 Ссылка ➡️ https://t.me/addlist/9wQJPILNMKNkNmNk 👉 Делимся знаниями и аудиторией — растём вместе ⚡️

CSS anchor-positioning: тултипы с привязкой к контенту без ResizeObserver Долгое время позиционирование тултипов и попапов в 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 и сложные анимации пока требуют гибридного подхода.

Gemini vs ChatGPT: СМЕНА ФАВОРИТОВ ... вот что вышло 👇 * Все вокруг обсуждают ChatGPT, а я нашел альтернативу, которая реаль
Gemini vs ChatGPT: СМЕНА ФАВОРИТОВ ... вот что вышло 👇 * Все вокруг обсуждают ChatGPT, а я нашел альтернативу, которая реально качает — Gemini от Google. Пользуюсь и очень доволен. Почему стоит попробовать: ✔️ Бесплатно (базовая версия) ✔️ Контекст 2 млн токенов — загружайте хоть целые кодобазы ✔️ Понимает текст, картинки, видео и аудио ✔️ Дружит с Google Диском, Gmail и календарем ✔️ Код пишет на уровне топ-моделей Решил проверить его в деле — и не прогадал. Попросил Gemini найти для меня экспертные каналы по IT и AI, чтобы собрать чистое инфополе с нуля и не делать все вручную. Закинул ссылки на проверенных авторов, и нейросеть сама проанализировала сотни рекомендаций, отсеяв пустышки. Результат — готовая подборка из 20+ каналов с реальным опытом по: AI-воркфлоу, автоматизации, вайб-кодингу, промт-инжинирингу, RAG-системам, нейрогенерации, крипте и др. 🔗 Забирайте список в один клик 👇 https://t.me/addlist/9wQJPILNMKNkNmNk * Пишите в комменты — пробовали Gemini? Делитесь с друзьями впечатлениями и добавляйте подборку в свой актив 📌

content-visibility: auto и contain-intrinsic-size: рендеринг только в видимой зоне без JS Каталог на 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 и быстрый первый рендер без фреймворков.

Container Queries: Резиновые карточки товаров без медиазапросов по viewport Карточки товаров, которые адаптируются к ширине родителя, а не экрана — это @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 делают компоненты по-настоящему автономными — вместо адаптации к экрану они адаптируются к контексту, что повышает стабильность интерфейса и упрощает поддержку дизайн-систем.

Inertial scrolling и overscroll-behavior: управляем pull-to-refresh, bounce-эффектами и sticky-навигацией без js-костылей На 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.

Overflow:clip и scrollbar-gutter: стабильные макеты без выпадения контента при появлении скроллбара Замечали: страница грузится, контент уже почти встал, и тут появляется скроллбар — весь блок уезжает вправо на 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.

CSS light-dark(): адаптивная тёмная тема без дублирования цветов В 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 для старых браузеров.

CSS scroll-timeline: индикатор подписи внутри sticky без IntersectionObserver Знакомый сценарий: 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 без дёрганий на мобильных устройствах.

View Transitions API — SPA-подобные переходы без единой строчки JS Раньше плавная навигация между страницами требовала сборку
View Transitions API — SPA-подобные переходы без единой строчки JS Раньше плавная навигация между страницами требовала сборку на 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, сохраняя семантику, доступность и простоту статической верстки.

text-wrap: balance и text-box-trim: точный контроль многострочного текста без кастомных JS-обрезок и magic-отступов Странное
text-wrap: balance и text-box-trim: точный контроль многострочного текста без кастомных JS-обрезок и magic-отступов Странное чувство, когда в 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-сетках — это шаг к стабильному и предсказуемому дизайну без хаков.

field-sizing: content — авто-высота input и textarea без единой строчки JS Раньше динамическую высоту полей ввода подстраивал
field-sizing: content — авто-высота input и textarea без единой строчки JS Раньше динамическую высоту полей ввода подстраивали через 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, делая адаптивную высоту предсказуемой и поддерживаемой в любой сторонке.