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

Личный блог автора - @just_genych
По вопросам рекламы или разработки: @g_abashkin
Download Telegram
Variadic tuple types — сложные сигнатуры без боли

До variadic tuple types
многие сложные сигнатуры в TypeScript
выглядели как наказание.

Особенно:

👉 curry
👉 compose
👉 middleware
👉 typed event emitter
👉 любые функции с «прокинь аргументы дальше»


Приходилось писать overload на overload
и дублировать типы вручную.


Как было раньше

Обычно появлялись:

👉 overload на overload
👉 ручные tuple-типы
👉 тонны дублирования

Типы быстро превращались
в нечитаемую простыню.

Что изменили variadic tuples

С их появлением стало намного проще
работать с остаточными аргументами на уровне типов.

Например:


type Fn<T extends unknown[]> =
(...args: [...T]) => void


Или собирать сигнатуры:


type Append<Args extends unknown[], Arg> =
[...Args, Arg]



Типы наконец научились нормально работать
с «переменным количеством аргументов».


Почему это важно

На практике это одна из тех TS-фич,
которые реально упростили жизнь библиотекам.

Без variadic tuples:

👉 Redux middleware typings
👉 router APIs
👉 compose/curry utilities

были бы ещё страшнее.

Где начинается тёмная магия

Проблемы начинаются,
когда variadic tuples комбинируют с:

👉 infer
👉 recursive types
👉 conditional types


Типовая система очень быстро
превращается в тёмный лес.


IDE начинает тормозить,
ошибки становятся нечитаемыми,
а compile time — расти.

Главная мысль

Variadic tuple types —
это действительно мощная фича.


Главное —
вовремя остановиться
и не превратить типы в отдельный язык программирования.
Страшная тайна российского айти

✖️ xCode Journal
Please open Telegram to view this post
VIEW IN TELEGRAM
😁71
CSS Anchor Positioning в продакшене: поповеры и тултипы без JS-позиционирования и вечной борьбы с координатами

Popover в кабинете, SaaS-интерфейсе или дизайн-системе часто ломается не из-за UI, а из-за геометрии. Типичная ошибка - тащить в каждый tooltip getBoundingClientRect(), scroll/resize handlers и свой мини layout engine.

Идея

CSS Anchor Positioning позволяет привязать один элемент к другому и описать размещение в CSS:


Actions

.trigger { anchor-name: --actions; }

.popover {
position: fixed;
position-anchor: --actions;
position-area: bottom center;
position-try-fallbacks: flip-block;
margin-top: 8px;
}


Что уходит из JS

* anchor-name объявляет кнопку якорем
* position-anchor связывает overlay с якорем
* position-area задает сторону и выравнивание
* position-try-fallbacks дает браузеру шанс переставить popover, если снизу нет места

В связке с Popover API HTML отвечает за открытие, закрытие и top layer, CSS - за геометрию, JS - за бизнес-логику.

Продакшен-подход

Используйте как progressive enhancement:

@supports (anchor-name: --x) and (position-area: bottom) {
.popover {
position-anchor: --actions;
position-area: bottom center;
}
}

Практический совет: оставьте fallback - обычное позиционирование, modal-like поведение или существующий overlay-компонент из дизайн-системы.

Где осторожно

Технология хороша для меню у кнопки, dropdown, tooltip у иконки, inline-подсказок форм. Но для nested-popover, виртуализированных списков, кастомной collision-логики и полной кроссбраузерной поддержки пока рано выкидывать Floating UI или Popper.

Вывод:

CSS Anchor Positioning убирает самый грязный слой overlay-архитектуры - JS-позиционирование, но требует feature detection, fallback и проверки доступности.
@starting-style + transition-behavior: enter/exit-анимации для dialog и popover без JS-классов

В production-модалках, меню, кабинетах, design systems и SaaS-интерфейсах анимация часто ломалась из-за display, top layer и быстрых повторных кликов. Типичная ошибка - хранить фазы в JS-классах и ждать transitionend.

Что меняется

@starting-style задаёт старт для элемента, который только появился: dialog[open] или :popover-open. transition-behavior: allow-discrete позволяет удержать дискретные display и overlay до конца exit-анимации, включая ::backdrop.

dialog,[popover]{
opacity:0;
transform:translateY(12px) scale(.98);
transition:
opacity .2s,
transform .2s,
display .2s allow-discrete,
overlay .2s allow-discrete;
}

dialog[open],
[popover]:popover-open{
opacity:1;
transform:none;
}

@starting-style{
dialog[open],
[popover]:popover-open{
opacity:0;
transform:translateY(12px) scale(.98);
}
}


Где trade-off

Состояние остаётся у платформы: popover может открываться через popovertarget, а для dialog нужны только showModal()/close(). Практический совет: анимируйте opacity и transform, а display/overlay добавляйте только как discrete-свойства для стабильного выхода.

Предупреждение

@starting-style нужен для входа, не для выхода. Exit берётся из закрытого состояния. Без allow-discrete элемент исчезнет мгновенно, и регрессия проявится на быстрых кликах, слабых устройствах или в компонентной библиотеке.

Вывод:
Для dialog и popover лучше держать состояние в нативных механизмах, а анимационные фазы описывать в CSS без лишних JS-классов.
Совет на ближайшие годы — изучайте ВАЙБ-КОДИНГ

ИИ уже пишет код, чинит баги, генерирует тесты, документацию и помогает запускать продукты быстрее, чем это делали классические команды разработки. И это уже не "будущее когда-нибудь", а реальность, которая меняет рынок уже сегодня

И те, кто научится вайбкодить сейчас, будут увереннее конкурировать на рынке и зарабатывать больше тех, кто по-прежнему делает всё вручную.

Стартовать с нуля поможет канал Вайб-кодинг. Там ребята круглосуточно мониторят более 320 российских и зарубежных источников и публикуют только главное: релизы, инструменты, гайды, курсы и практические кейсы.

Подписывайтесь, нас уже 45 тысяч: @vibecoding_tg
1
⚡️ content-visibility: auto + contain-intrinsic-size: ускоряем длинные страницы без виртуализации и CLS

Если на странице много тяжёлых блоков — карточки, статьи, отзывы, секции лендинга, документация — не всегда нужна виртуализация. Иногда достаточно сказать браузеру: «не рендери то, что далеко за экраном».

Для этого есть:

.content-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 480px;
}


Что происходит:

- content-visibility: auto разрешает браузеру пропускать рендеринг offscreen-блоков;
- layout/paint/style откладываются до момента, когда блок приблизится к viewport;
- DOM остаётся на месте: это не виртуализация, элементы не удаляются;
- contain-intrinsic-block-size задаёт резервный размер, пока реальная высота ещё не посчитана.

Главный нюанс — CLS.

Если просто повесить:

.card {
content-visibility: auto;
}


браузер может не знать, сколько места должен занимать пропущенный блок. Когда пользователь доскроллит до него, блок отрендерится, получит реальную высоту и может сдвинуть всё ниже.

Поэтому вместе с content-visibility почти всегда нужен intrinsic size:

.article-preview {
content-visibility: auto;
contain-intrinsic-block-size: auto 360px;
}


Здесь 360px — fallback-оценка высоты до первого рендера.
Ключевое слово auto позволяет браузеру запомнить реальный размер после рендера и дальше использовать уже его.

Практический пример:

<main class="feed">
<article class="feed-card">...</article>
<article class="feed-card feed-card--large">...</article>
<article class="feed-card">...</article>
</main>


.feed-card {
content-visibility: auto;
contain-intrinsic-block-size: auto 320px;
}

.feed-card--large {
contain-intrinsic-block-size: auto 560px;
}


Лучше не подбирать один размер для всего. Если блоки сильно отличаются, разбивайте их на классы: обычная карточка, большая карточка, промо-блок, секция с галереей и т.д.

Где это особенно полезно:

- длинные ленты без интерактивной виртуализации;
- страницы документации;
- каталоги с тяжёлыми карточками;
- лендинги с большим количеством секций;
- SSR-страницы, где DOM уже есть, но рендерить всё сразу дорого.

Где быть осторожнее:

- above-the-fold блоки — им это обычно не нужно;
- sticky/anchor-зависимые интерфейсы;
- элементы, размер которых критично влияет на соседей;
- списки на десятки тысяч DOM-узлов — тут виртуализация всё ещё может быть нужна;
- сложные случаи с измерением размеров через JS.

Важно: content-visibility: auto не уменьшает количество DOM-элементов и не отменяет стоимость JS, обработчиков событий или хранения данных в памяти. Он оптимизирует именно рендеринг: layout, paint и часть работы вокруг них.

Хороший паттерн:

@supports (content-visibility: auto) {
.long-page-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 600px;
}
}


Так это становится progressive enhancement: браузеры с поддержкой получают ускорение, остальные рендерят страницу как обычно.

Итог: для длинных страниц, где не хочется усложнять архитектуру виртуализацией, content-visibility: auto — дешёвый способ снизить initial rendering cost. Но без contain-intrinsic-size легко получить layout shifts, поэтому эти свойства почти всегда стоит использовать парой.
👍1
Cascade Layers в дизайн-системе: reset, vendors и utilities без войн специфичности

В дизайн-системах для SaaS, e-commerce и кабинетов reset, CSS из npm-пакетов, компоненты и utilities быстро начинают конкурировать. Частая ошибка - лечить это через !important, .foo.foo и порядок импортов.

Контракт слоев
Сначала зафиксируйте каскад явно:

@layer reset, vendors, tokens, base, components, utilities, overrides;


Чем слой правее - тем выше приоритет при одинаковом origin/importance. Специфичность работает внутри слоя, но между слоями сначала побеждает порядок @layer.

Production-пример
@import url("./reset.css") layer(reset);
@import url("./datepicker.css") layer(vendors);

@layer components {
.button { background: var(--accent); }
}

@layer utilities {
.bg-danger { background: var(--danger); }
}


Теперь .bg-danger предсказуемо переопределит фон компонента, даже если селектор компонента специфичнее. Не нужно делать .bg-danger.bg-danger или надеяться, что файл utilities подключится последним.

Практичная схема
* reset - нормализация браузерных стилей.
* vendors - datepicker, select, editor, carousel.
* tokens - CSS custom properties и темы.
* base - типографика и дефолты тегов.
* components - button, input, modal, tabs.
* utilities - атомарные override-классы.
* overrides - интеграционные фиксы, которые должны быть видимыми и редкими.

Важные нюансы
Стили вне @layer сильнее обычных layered-стилей. Поэтому договоритесь: весь CSS дизайн-системы живет в слоях, а приложение переопределяет ее через свой верхний слой.

@layer app-overrides;

@layer app-overrides {
.checkout .button { min-width: 240px; }
}


Предупреждение: у !important порядок слоев инвертируется. Не строите архитектуру вокруг него - layers нужны, чтобы снижать потребность в таких патчах.

Вывод:
@layer превращает каскад из побочного эффекта сборки в явный API дизайн-системы, повышая предсказуемость layout и снижая визуальные регрессии.
Web Locks API в браузере: cross-tab mutex для refresh token, миграций и защиты от гонок

Когда одна сессия открыта в нескольких вкладках, frontend, SPA, SSR-клиенты и SDK начинают делить auth state, storage и кэш. Частая ошибка - защищать refresh token только in-memory single-flight: между вкладками он не работает.

Refresh token как критическая секция
Если 5 вкладок одновременно получили 401, они могут отправить несколько refresh-запросов одним token, получить invalid_grant и перетереть свежие токены старыми. Web Locks API дает mutex на один origin:

type Tokens = {
accessToken: string;
refreshToken: string;
};

const LOCK = 'auth:refresh-token';

async function ensureAccessToken(): Promise<string> {
return navigator.locks.request(LOCK, async () => {
const latest: Tokens = await loadTokens();

if (!isExpiring(latest)) {
return latest.accessToken;
}

const next = await refreshTokens(latest.refreshToken);
await saveTokens(next);

return next.accessToken;
});
}


Важный паттерн
После входа в lock нужно перечитать storage. Пока вкладка ждала, другая уже могла обновить токены. Практический совет: внутри критической секции всегда делайте double-check precondition, а не запускайте refresh сразу после ожидания.

Где еще полезно
Web Locks хорошо ложится на миграции IndexedDB/localStorage, одноразовую инициализацию кэша, защиту записи в общий browser storage и координацию фоновых задач между вкладками.

Ограничения
Lock работает только в пределах origin. Это не distributed lock и не замена серверной идемпотентности. Критическая секция должна быть короткой; для сетевого refresh ставьте timeout. Если navigator.locks нет, нужен fallback через BroadcastChannel, storage lease или серверную защиту.

Вывод:
Web Locks API полезен там, где несколько вкладок делят runtime-состояние: он снижает риск гонок, но не отменяет серверные гарантии.
CSS Scroll-Driven Animations уже можно аккуратно тащить в продакшен — особенно для двух задач, где раньше часто ставили IntersectionObserver или scroll-listener:

1. reading progress bar
2. sticky-reveal секции

Главная идея: прогресс анимации привязан не ко времени, а к скроллу.

animation-timeline: scroll(root block);


или к появлению конкретного блока во viewport:

animation-timeline: --section;
animation-range: entry 15% cover 45%;


То есть браузер сам считает progress timeline, а мы описываем только CSS-анимацию.

Пример: progress bar + sticky-reveal без JS

<div class="read-progress"></div>

<section class="feature">
<div class="feature__sticky">
<h2>Scroll-driven reveal</h2>
<p>Блок раскрывается по мере прохождения секции.</p>
</div>
</section>


.read-progress {
position: fixed;
z-index: 1000;
inset: 0 0 auto;
block-size: 3px;
background: #7c3aed;
transform-origin: 0 50%;
transform: scaleX(0);
display: none;
}

.feature { min-block-size: 180vh; }

.feature__sticky {
position: sticky;
top: 20svh;
opacity: 1;
transform: none;
}

@supports (animation-timeline: scroll()) {
.read-progress {
display: block;
animation: progress linear both;
animation-timeline: scroll(root block);
}

@keyframes progress {
to { transform: scaleX(1); }
}

.feature {
view-timeline-name: --feature;
view-timeline-axis: block;
}

.feature__sticky {
opacity: 0;
transform: translateY(24px) scale(.98);
animation: stickyReveal linear both;
animation-timeline: --feature;
animation-range: entry 15% cover 45%;
will-change: opacity, transform;
}

@keyframes stickyReveal {
to {
opacity: 1;
transform: none;
}
}

@media (prefers-reduced-motion: reduce) {
.read-progress,
.feature__sticky {
animation: none;
}

.feature__sticky {
opacity: 1;
transform: none;
}
}
}


Что важно для продакшена:

• Заворачивать в @supports
Scroll-Driven Animations пока не стоит считать безусловным baseline. Делайте progressive enhancement: без поддержки пользователь просто видит обычный контент.

• Не прятать контент по умолчанию
Плохой паттерн:

.card {
opacity: 0;
}


А потом раскрывать только внутри scroll-animation. В браузере без поддержки блок останется невидимым. Лучше базово показывать контент, а стартовое состояние задавать внутри @supports.

• Анимировать дешёвые свойства
transform и opacity — ок.
height, top, margin, layout-зависимые штуки — осторожно. Scroll-driven не делает дорогую анимацию бесплатной.

IntersectionObserver всё ещё нужен не всегда, но и не умер
Для визуальных reveal/progress эффектов CSS часто проще и чище. Но если нужно грузить данные, отправлять аналитику, управлять состоянием приложения или синхронизироваться с бизнес-логикой — это всё ещё зона JS.

• Для sticky-reveal лучше таймлайн секции, а не самого sticky-элемента
Sticky-элемент может визуально «залипать», поэтому удобнее повесить view-timeline-name на родительскую секцию, а анимировать внутренний sticky-блок через named timeline.

Итог: для progress bars и декоративных sticky-reveal эффектов Scroll-Driven Animations позволяют убрать лишний JS, не плодить observers/listeners и оставить поведение декларативным. Главное — progressive enhancement, prefers-reduced-motion и нормальный fallback.
2
CSS Subgrid в продакшене: выравниваем вложенные формы и карточки без дублирования сеток

В кабинетах, checkout, SaaS-формах и дизайн-системах часто нужно, чтобы вложенные секции попадали в колонки общего layout. Типичная ошибка - копировать grid-template-columns в каждый компонент и потом ловить рассинхрон на адаптиве.

Где помогает subgrid

subgrid позволяет вложенному grid использовать треки непосредственного родителя по колонкам или строкам. Это полезно, когда форма разбита на группы, но label, input и action-кнопки должны стоять по одной визуальной сетке.

.settings {
display: grid;
grid-template-columns: 160px minmax(0, 1fr) max-content;
gap: 12px 16px;
}

.group {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
gap: inherit;
}

.group > input {
grid-column: 2;
min-width: 0;
}


Production-смысл

Так сетка описывается один раз в контейнере, а вложенные формы, строки настроек или карточки наследуют ее геометрию. Меньше CSS-дублей - меньше риска, что после изменения breakpoint'а кнопки, поля и подписи начнут визуально разъезжаться.

Хорошие кандидаты:
• большие формы настроек
• checkout и billing flows
• dashboard-строки со сложной вложенностью
• карточки в дизайн-системе с общей action-зоной

Ограничение, о котором часто забывают

subgrid работает только от непосредственного grid-контейнера, а сам элемент должен быть grid item и занимать нужный диапазон:

.card {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
}


Недостаточно просто написать grid-template-columns: subgrid внутри любого компонента - без родительской grid-сетки он не даст ожидаемого выравнивания.

Практический паттерн

Используйте subgrid там, где цена дублирования сетки реальна, и держите предсказуемый fallback:

.form-group {
display: grid;
gap: 12px;
}

@supports (grid-template-columns: subgrid) {
.form-group {
grid-column: 1 / -1;
grid-template-columns: subgrid;
}
}


Fallback может быть обычной вертикальной формой. Интерфейс не ломается, а современные браузеры получают точное выравнивание.

Вывод:
subgrid стоит тащить в продакшен не ради синтаксиса, а там, где он снижает дублирование CSS и повышает стабильность сложного layout.
2
100vh на мобильных все еще ломает full-screen интерфейсы: dvh/svh/lvh и safe-area для CTA без обрезания

В лендингах, paywall, onboarding, SaaS-кабинетах и дизайн-системах первый экран часто должен занять видимую высоту. Типичная ошибка - ставить height: 100vh и получать CTA под нижней панелью браузера или home indicator.

Единицы viewport
100svh - высота при раскрытых панелях браузера: безопасно, но может дать запас.
100lvh - высота при скрытых панелях: годится для фона, рискованна для контента.
100dvh - текущая видимая высота: хороший выбор для экранов состояния.
env(safe-area-inset-bottom) - защита CTA от системных зон.

Production-паттерн
.screen{
min-height:100vh;
min-height:100svh;
min-height:100dvh;
display:flex;
flex-direction:column;
padding:max(16px,env(safe-area-inset-top)) 16px max(16px,env(safe-area-inset-bottom));
}
.screen__content{flex:1;min-height:0}
.screen__cta{position:sticky;bottom:max(16px,env(safe-area-inset-bottom))}


Trade-off
dvh пересчитывается при изменении browser chrome и может давать визуальные скачки. Не вешайте height: 100dvh на длинные страницы: для hero, modal screen и onboarding чаще безопаснее min-height, чтобы контент мог вырасти.

Практическое правило
• full-screen состояние - min-height: 100dvh
• максимально стабильный первый экран - 100svh
• декоративный фон - можно lvh
• нижний CTA - padding/bottom через max(..., env(safe-area-inset-bottom))

Вывод:
Мобильная верстка больше не сводится к 100vh: выбирайте viewport-единицу под сценарий и всегда резервируйте safe-area.
2👍1
field-sizing: content — авто-высота input и textarea без единой строчки JS

Раньше динамическую высоту полей ввода подстраивали через scrollHeight и обработчики событий. В production — формы обратной связи, комментарии в лендингах, кабинетах, SaaS-интерфейсах — это приводило к лишним рекалькуляциям и багам с resize. Частая ошибка — забывать сбрасывать замеры при скрытии полей или анимациях.

Как работает
field-sizing: content заставляет браузер сам рассчитывать высоту по содержимому. Никакого JS, только CSS:
textarea, input[type="text"] {
field-sizing: content;
min-height: 2em;
max-height: 10em;
}

Свойство применимо к textarea и текстовым input (text, email, search, tel, url, password). select не поддерживается. Важно: resize при этом не ломается, пользователь всё ещё может растягивать поле.

Production-применение
Поля обратной связи, комментарии, списки задач — везде, где контент непредсказуем. Особенно ценно в design systems и компонентных библиотеках, где высоту нельзя захардкодить. Вместо ResizeObserver и багов с размерами под скроллингом — предсказуемый layout.

Типичная ошибка
Полагаться только на field-sizing без ограничителей. Если контент длинный, поле заметно сдвинет соседние элементы. Всегда ставь min-height и max-height, чтобы контролировать стабильность интерфейса и избежать внезапного визуального регресса.

Trade-offs
JS всё ещё нужен для анимации расширения, поддержки старых браузеров (Firefox пока частично) или кастомных элементов. Но для типовых полей — это шаг к CSS как инструменту поведения, а не только оформления.

Вывод:
Инженерный takeaway — field-sizing: content убирает связку JS + scrollHeight, делая адаптивную высоту предсказуемой и поддерживаемой в любой сторонке.
text-wrap: balance и text-box-trim: точный контроль многострочного текста без кастомных JS-обрезок и magic-отступов

Странное чувство, когда в Figma заголовок выглядит ровно, а в браузере последняя строка уходит в одну букву. Или когда карточка с текстом плывёт из-за того, что line-height даёт лишние отступы сверху и снизу. Это частая проблема в лендингах, кабинетах и дизайн-системах, где каждый пиксель важен для предсказуемости layout.

Раньше с этим воевали костылями. JS-скрипты, которые считают ширину строк и вставляют <br>. Или обнуление line-height с ручным подбором margin. В 2024 году можно проще.

Балансировка строк без JS
text-wrap: balance берёт на себя балансировку строк. Браузер сам решает, где разорвать текст, чтобы все строки были примерно одинаковой длины. Особенно видно на заголовках из 2-4 строк. Классический пример — блок .title { text-wrap: balance; }. Но не кидайте это на параграфы. На длинных текстах браузер будет считать разрывы для каждой строки, и нагрузка растёт. Ошибка: вешать на все блоки подряд — теряете производительность на пересчёте.

Удаление магических отступов
Вторая штука — text-box-trim. Пока экспериментальная, работает в Safari и Chrome Canary. Убирает те самые «магические» отступы, которые line-height добавляет над первой строкой и под последней. Раньше приходилось выставлять отрицательный margin с дробными значениями. Теперь: text-box-trim: both; text-box-edge: cap alphabetic;. Полезно в карточках, кнопках, списках — сетка перестаёт плыть. Совет: тестируйте в production на проектах с поддержкой только современных браузеров.

Trade-offs и поддержка
Минусы: balance не стоит вешать на всё подряд, только на короткие блоки. text-box-trim пока не завезли в стабильные браузеры. Но спецификация движется, и через пару лет это будет базой. Предупреждение: не используйте balance на hero-секциях с большим текстом — браузер может подтормаживать на мобильных.

Вывод:
Использование text-wrap: balance для заголовков и text-box-trim для точного выравнивания текста в layout-сетках — это шаг к стабильному и предсказуемому дизайну без хаков.
View Transitions API — SPA-подобные переходы без единой строчки JS

Раньше плавная навигация между страницами требовала сборку на SPA или извращения с fetch и ручной заменой DOM. View Transitions API ломает это правило — браузер сам снимает снэпшоты и анимирует переходы на обычном HTML/CSS. Частая ошибка: полагать, что MPA не может быть плавной, и тянуть тяжелый фреймворк для простого лендинга или статического сайта.

Базовый запуск — мета-тег
Просто добавьте в <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-потоке.

Свойство 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-проектах — лендингах, админ-панелях, дизайн-системах — тёмная тема часто реализуется через @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 для старых браузеров.
🔥21
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