Логово верстальщика
8.22K subscribers
1.04K photos
49 videos
4 files
1.85K links
Логово верстальщиков: HTML, CSS, JavaScript, практики современной верстки, вайбкодинг и использование ИИ в разработке.

Личный блог автора - @just_genych
По вопросам рекламы или разработки: @g_abashkin
Download Telegram
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.
1🔥1
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.
1
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 делают компоненты по-настоящему автономными — вместо адаптации к экрану они адаптируются к контексту, что повышает стабильность интерфейса и упрощает поддержку дизайн-систем.
👍1
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 и быстрый первый рендер без фреймворков.
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 и сложные анимации пока требуют гибридного подхода.
👎1🔥1
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 и доступность важнее универсальной браузерной поддержки.
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.
👍1
@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.
👍1
Наконец-то нормальная анимация 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+.
👍31
🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы

Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли.

Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM.

✖️ xCode Journal
Please open Telegram to view this post
VIEW IN TELEGRAM
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 вместо целого скрипта — чище код, быстрее загрузка, меньше багов в формах.
👍2
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.
.card:has(.card__button:focus) — селектор, который выкидывает BEM-костыли на свалку

Помните ту боль, когда для тени на карточке из-за фокуса кнопки приходилось лезть в 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-переменных под каждый режим

Многие верстальщики по привычке заводят десятки переменных в :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
🤣 Общаться о синтаксисе теперь тоже можно через агентов

✖️ xCode Journal
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

Привязывает анимацию к прогрессу прокрутки контейнера. Например, прогресс-бар чтения статьи заполняется по мере скролла страницы — без единого обработчика.

@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-гаданий.

.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 работает корректно — элемент "вырывается" наружу, а лишнее скрывается.

.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-контекста, который стоит использовать, когда нельзя жертвовать скроллом страницы.
👍31
Card component isolation with container-type: size vs container: none
Вы закинули карточку в сайдбар — она сжалась. Переставили в сетку — та же карточка растянулась, но ломает сетку. Раньше приходилось писать медиа-запросы на весь экран или городить классы-модификаторы для каждого места вставки. Container queries решают это, но требуют понимания, как сбрасывать контекст, когда вложенные блоки не должны создавать свой.

Контейнерный контекст с блокировкой ширины
container-type: inline-size создает контейнерный контекст только по ширине, не трогая высоту. Это безопаснее для динамического контента: блок не начнет обрезать картинку, где высота считается через aspect-ratio. Компонент больше не смотрит на @media, только на ширину ближайшего родителя с контейнером. Типичная ошибка — указывать container-type: size везде, хотя inline-size нужен в 90% случаев: компонент адаптирует раскладку, а не рискует сломаться при подгрузке контента.

Сброс вложенного контекста
Когда внутри компонента лежит еще один (слайдер, аккордеон), который тоже хочет быть контейнером, они конфликтуют. Внутренний блок ловит свой размер, игнорируя внешний. Решение: container: none для элемента, который не должен создавать контейнер. Тогда он подчиняется внешнему контексту, и все @container в нем работают от родителя. Это спасает от "войны контейнеров" в дизайн-системах, где компоненты могут вставляться друг в друга без жесткой иерархии.

Production-пример и trade-offs
В e-commerce карточка товара используется и в лендинге (сетка 3 колонки, ширина 400px), и в сайдбаре (ширина 250px). Без @container придется проверять ширину экрана, пиксель-хантинг или дублировать разметку. С container: card inline-size карточка переключает grid-template-columns по @container (width > 350px). Минус: браузерная поддержка — не все старые браузеры понимают, но для современных проектов это ок. Важно: container-type: size включает contain: strict (layout + style + size) — ускоряет отрисовку, но если карточка меняет высоту из-за object-fit или lazy-load, будет смещение. inline-size решает это.

@container card (width > 350px) {
.card {
grid-template-columns: auto 1fr;
gap: 1rem;
}
}
.card--nested {
container: none;
}


Вывод: Изолируйте компонент от глобального экрана с container-type: inline-size и сбрасывайте вложенные контексты через container: none — это делает архитектуру предсказуемой и убирает зависимость от мест вставки.
👍1
Как я собираю мини‑аналитику по рынку профессий

Давно работая с HR‑аналитикой, мне стало интересно не просто смотреть на рынок, но и самому выделять основное, что с него можно собрать, и представлять новые данные в новых разрезах — зарплатную аналитику, аналитику подбора персонала и тому подобное.

Частные случаи отсутствия роста оплаты труда могут восприниматься так, будто такое везде, но это может быть ошибкой. Год назад была достаточно сильная гонка зарплат, которая сейчас привела к акценту на производительности труда в стране. Многие ее не заметили. Таких кейсов много: безработица низкая, значит, дефицит кадров. Но сейчас не дефицит кадров вообще, а дефицит квалифицированных кадров и дефицит рабочих. Без данных такие фразы превращаются в ощущения, а ощущения — плохая основа для выводов.

Поэтому я начал собирать небольшой аналитический проект по рынку профессий. Идея простая: брать открытые данные, аккуратно приводить их в порядок и собирать короткие профили по отдельным профессиям.

Читать далее
Please open Telegram to view this post
VIEW IN TELEGRAM