Лекция 4. Современный CSS

Веб-программирование

Лекция 4 Современный
CSS

Веб-программирование · лекция 4 из 16

На прошлой лекции

🌊Потокчто браузер делает сам и делает разумно
↔️Flexboxодна ось, выравнивание, гибкость
▦Gridдве оси, сетка задана заранее

Научились ставить элементы на места. Сегодня учим страницу подстраиваться и двигаться.

Обещание с прошлого раза

grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

«Адаптивность без единого медиазапроса». Сетка сама решает, сколько колонок влезет.

Хорошо для галереи. Но у страницы есть шапка, меню, типографика и картинки — и сами по себе они подстраиваться не умеют.

Кто открывал
свою ЛР
на телефоне?

И какого размера там был текст

Микроскопический текст

У половины зала страница из ЛР на телефоне выглядит как уменьшенная копия компьютерной: всё на месте, но читать можно только с лупой.

Это не ошибка в вашем CSS. Это решение браузера, и сегодня мы узнаем, почему он так решил и как его переубедить.

Ожидание и реальность

Моя ЛР

Привет!

Крупный текст, одна колонка, кнопку видно сразу.

Читать дальше
как задумано
Моя ЛР · Главная · Обо мне · Проекты · Контакты

Привет! Это моя страница. Здесь крупный текст, одна колонка и кнопку видно сразу.

Ещё немного текста мелким шрифтом, который никто не прочитает без лупы.

как открылось на телефоне

Сегодня за 90 минут

Пять тем, и все пять сойдутся в одной маленькой карточке.

Часть 1 · 13 минут CSS-переменные

Один раз объявить

Сорок синих

.header  { background: #4b86c2; }
.button  { background: #4b86c2; }
.link    { color: #4b86c2; }
.tab     { border-bottom: 3px solid #4b86c2; }
/* ...и ещё тридцать шесть раз по файлу */

Дизайнер просит «синий чуть темнее». Поиск и замена по проекту — и одно место обязательно окажется #4B86C2 заглавными.

Объявить один раз

:root {
    --accent: #4b86c2;
}

.button { background: var(--accent); }
.link   { color: var(--accent); }

Объявляется как обычное свойство, но с двумя дефисами в начале. Читается функцией var().

Почему два дефиса

Чтобы имя никогда не совпало со стандартным свойством — ни сегодняшним, ни тем, что появится через десять лет. Всё, что начинается с --, браузер гарантированно считает вашим.

Регистр важен: --Accent и --accent — две разные переменные. Обычные свойства так себя не ведут.

Название важнее, чем кажется

Custom properties — пользовательские свойства. Не «переменные» в смысле языков программирования, а такие же свойства, как color.

Значит, на них работает всё из лекции 2: каскад, наследование, специфичность.

Где объявлять

:root

видно везде: переменная наследуется от корня всем потомкам

любой элемент

видно только ему и его потомкам: у каждого блока — свои значения

.card       { --accent: #4b86c2; }
.card.warm  { --accent: #d68910; }  /* только у тёплых карточек */

Три карточки, одна строка разницы

🎨
Лекция 4Современный CSS
🧰
Лекция 5CSS-инструменты
⚙️
Лекция 6JavaScript

CSS у карточек общий. Переопределена только переменная — и рамка, подложка и подпись перекрасились вместе.

карточка

Запасное значение

.button {
    /* если --accent нигде не объявлена — будет синий */
    background: var(--accent, #4b86c2);
}

Второй аргумент var() — на случай, если переменной нет. Удобно в компонентах, которые должны работать и без настройки.

Какой будет
фон?

Отвечаем вслух, потом проверим

Переменную забыли объявить

.box {
    background: blue;
    background: var(--bg);   /* --bg нигде нет */
}

Вторая строка сломана. Значит, сработает первая и фон будет синим?

Проверяем

Фон прозрачный. Не синий. Браузер проверяет правило при разборе файла — а тогда var(--bg) выглядит нормально, и вторая строка честно побеждает первую.

Подставляет значение браузер позже. Выяснилось, что подставлять нечего, а первая строка уже проиграла каскад. Откатиться некуда — свойство сбрасывается в начальное значение.

Ловушка с единицами

не работает

--gap: 20;
margin: var(--gap)px;

работает

--gap: 20px;
/* или посчитать: */
margin: calc(var(--n) * 1px);

var() подставляет не текст, а готовое значение: получается не 20px, а 20 px — число и отдельное слово.

Они живые

Переменную можно переопределить где угодно — и значение поменяется прямо в работающей странице:

.button:hover       { --accent: #33648f; }
@media (...)        { :root { --accent: ... } }   /* часть 2 */
el.style.setProperty('--accent', 'tomato');       // лекция 6

Не путать с переменными Sass

$accent — Sass

подставляется при сборке и исчезает: в браузер приходит уже готовый цвет

--accent — CSS

живёт в браузере: наследуется, переопределяется, меняется из скрипта

Про Sass — в лекции 5. Пока запомните: это разные вещи с похожим назначением.

2016

Пришли поздно, зато сразу всюду

Firefox поддержал их первым, в 2014-м. В 2016-м — Chrome и Safari, в 2017-м — Edge. Internet Explorer не поддержал никогда.

До этого переменные в CSS были главной причиной ставить препроцессор. Одну из двух причин они закрыли.

Дизайн-токены

:root {
    --color-accent: #4b86c2;
    --color-text:   #1a1a1a;
    --radius:       12px;
    --space:        16px;
    --shadow:       0 4px 12px rgb(0 0 0 / 0.1);
}

Всё, из чего состоит «стиль сайта», — в одном месте. Поменяли пять строк — поменялся весь сайт.

🥚 Проверьте на главной странице курса: hacker

Часть 2 · 26 минут Адаптивность

Один сайт на все экраны

2007

Веб оказался в кармане

Выходит iPhone. Экран — 320 точек в ширину. Сайты того времени свёрстаны на 960 пикселей, потому что «у всех мониторы 1024».

Первая реакция индустрии — сделать второй сайт. Отдельная мобильная версия на m.site.ru: другая разметка, другие стили, и вечно отстающая от основной.

2010

Responsive Web Design

Итан Маркотт в статье для A List Apart называет подход и собирает его из трёх ингредиентов:

Резиновая сеткаширины в процентах, а не в пикселях
Гибкие картинкине шире своего контейнера
Медиазапросыразные правила для разных экранов
Ethan Marcotte, «Responsive Web Design», A List Apart, 2010

Один сайт — три раскладки

одна колонка
две колонки
сайдбар + три

Одна разметка, один CSS-файл. Меняется только раскладка.

Почему текст был микроскопическим

Мобильный браузер не знает, готова ли страница к узкому экрану. На всякий случай он считает, что нет: рисует её в виртуальном окне шириной 980 пикселей и уменьшает целиком, чтобы влезла.

Для сайтов 2007 года это было спасением. Для вашей ЛР — нет: её медиазапросы видят ширину 980 и думают, что это компьютер.

Строка, которую все копировали не глядя

<meta name="viewport"
      content="width=device-width, initial-scale=1">

Она из лекции 1. Означает: «ширина окна — это настоящая ширина экрана, ничего не уменьшай». С ней телефон честно отдаёт странице свои 375 пикселей.

Без неё адаптивная вёрстка на телефоне не включается вообще.

А вот так — никогда

запрещает увеличивать

content="...,
  maximum-scale=1,
  user-scalable=no"

достаточно

content="width=device-width,
  initial-scale=1"

Запрет увеличения бьёт ровно по тем, кому оно нужнее всего, — как px вместо rem в лекции 3.

Пиксель — не пиксель

Долг с прошлой лекции. У iPhone 4 в 2010-м экран стал вдвое плотнее — и один CSS-пиксель стал равен двум физическим по каждой стороне.

window.devicePixelRatio   // 1 — обычный монитор, 2–3 — телефон

CSS-пиксель — единица длины, а не точка на матрице. Поэтому у телефона с 1170 физическими пикселями ширина окна — 390, и это нормально.

Медиазапрос

.cards { grid-template-columns: 1fr; }

@media (min-width: 768px) {
    /* включается, только если окно не уже 768px */
    .cards { grid-template-columns: 1fr 1fr; }
}

Блок обычных правил, который включается по условию. Условие проверяется постоянно: растянули окно — правила включились.

Грамматика

@media screen { ... }                    /* тип: screen, print */
@media (min-width: 600px) { ... }        /* условие */
/* И */
@media (min-width: 600px) and (orientation: landscape) { ... }
/* ИЛИ — через запятую */
@media (max-width: 400px), print { ... }
@media not print { ... }                 /* НЕ */

Условия в скобках, логика — словами. Почти как в любом языке программирования.

Специфичность не меняется

Правило внутри @media — это обычное правило с обычной специфичностью. Условие только решает, участвует ли оно в споре. Как спор решается — прежние правила каскада из лекции 2.

Запомните эту фразу: скоро она объяснит викторину.

2009

Mobile first — это не про CSS

Люк Вроблевски, дизайнер интерфейсов, предлагает проектировать сначала для телефона. Не потому что телефонов больше, а потому что маленький экран заставляет решить главное: что на странице действительно нужно.

Начните с важного — и широкий экран станет вопросом «что ещё добавить».

Luke Wroblewski, «Mobile First», A Book Apart, 2011

Mobile first

desktop first

.nav { display: flex; }
@media (max-width: 600px) {
  .nav { display: none; }
}

mobile first

.nav { display: none; }
@media (min-width: 600px) {
  .nav { display: flex; }
}

Сначала узкий экран, потом min-width добавляет. Не наоборот.

Два направления

desktop first · max-width по убыванию

база
− 900 − 600
база
− 900
база
(широкий макет)

mobile first · min-width по возрастанию

база
(узкий макет)
база
+ 600
база
+ 600 + 900
3206009001400

Слева направо — ширина окна. В первом случае узкий экран отменяет широкий, во втором широкий добавляет к узкому.

Одна задача — двумя способами

desktop first

.cards { grid-template-columns: repeat(3, 1fr); }
aside  { display: block; }

@media (max-width: 899px) {
  .cards { grid-template-columns: 1fr 1fr; }
  aside  { display: none; }
}
@media (max-width: 599px) {
  .cards { grid-template-columns: 1fr; }
}

mobile first

.cards { grid-template-columns: 1fr; }
aside  { display: none; }

@media (min-width: 600px) {
  .cards { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 900px) {
  .cards { grid-template-columns: repeat(3, 1fr); }
  aside  { display: block; }
}

Строк поровну. Разница в другом: что получит телефон, если медиазапрос не сработал, — три сплющенные колонки или одну читаемую.

Почему именно так

Поток из лекции 3 уже одноколоночный. Mobile first — это просто «не мешай потоку, пока экран узкий».

Какая ширина
на экране 1200?

Два медиазапроса

.col { width: 100%; }

@media (min-width: 900px) { .col { width: 33%; } }
@media (min-width: 600px) { .col { width: 50%; } }

Окно — 1200 пикселей. Три колонки или две?

Проверяем

Две: 50 %. При ширине 1200 истинны оба условия. Специфичность одинаковая — побеждает то, что написано позже.

Каскад из лекции 2, никакой магии. Отсюда правило: при mobile first медиазапросы идут по возрастанию ширины.

Граница на стыке

@media (max-width: 600px) { ... }   /* до 600 включительно */
@media (min-width: 600px) { ... }   /* от 600 включительно */

На ширине ровно 600 срабатывают оба. Раньше писали 599.98px. Сейчас есть синтаксис диапазонов:

@media (width < 600px)          { ... }
@media (600px <= width < 900px) { ... }

Где ставить брейкпоинты

600px 900px одна колонка две три и сайдбар

Не под модели телефонов — их сотни, и через год будут другие. Тяните окно, пока макет не станет некрасивым: там, где ломается контент, и брейкпоинт.

Откуда берут числа

по устройствам375, 390, 393… — устаревает с каждым новым телефоном
по фреймворкуBootstrap: 576 768 992 1200
Tailwind: 640 768 1024 1280
по контентутам, где строка стала длиннее 75 знаков или меню не влезло

Числа фреймворков — нормальная отправная точка, если своих причин нет. Но причина всегда в контенте: у фреймворка просто кто-то уже подумал за вас.

Брейкпоинты по-взрослому

🙅
max-width: 393px — «это под iPhone 15»
👉
width < 38em — «здесь меню перестаёт влезать»

Первый брейкпоинт устареет с выходом следующего телефона. Второй — никогда: он про ваш контент, а не про чужое железо.

Брейкпоинты — в em

@media (min-width: 40em) { ... }   /* 640px при шрифте 16px */

em в медиазапросе считается от шрифта браузера по умолчанию. Человек увеличил шрифт до 24px — брейкпоинт сам сдвинулся на 960, и на ноутбуке он получит просторный узкий макет вместо трёх колонок с огромными буквами.

px так не умеет. Снова та же доступность, что в лекции 3.

Не гадать, а посмотреть

В DevTools — режим устройства: Ctrl+Shift+M, на Mac Cmd+Shift+M. Ширина ползунком, пресеты телефонов, плотность пикселей.

Над страницей — полоса с вашими же медиазапросами: щёлкните по отрезку, и окно встанет ровно на эту ширину.

Третий инструмент DevTools в курсе — после значков flex и grid.

Спрашивать можно не только ширину

orientationпортрет или пейзаж
hoverумеет ли устройство наводить
pointerмышь точная, палец — нет
printстраницу печатают
prefers-*настройки человека
resolutionплотность пикселей

Медиазапрос спрашивает не про размер, а про обстоятельства.

Что будет
с :hover
на телефоне?

Залипает

Навести пальцем нельзя, поэтому телефон считает наведением тап — и оставляет его «наведённым», пока не тапнешь в другое место. Кнопка так и висит красной.

@media (hover: hover) {
    .card:hover { translate: 0 -4px; }
}

Эффект включается только там, где им правда можно пользоваться.

Настройки человека

prefers-color-schemeсветлая или тёмная тема в системе
prefers-reduced-motionпросьба поменьше анимаций
prefers-contrastпросьба повысить контраст

Человек один раз выбирает в настройках системы — и каждый сайт, который спрашивает, узнаёт ответ. Проверить без смены настроек: DevTools → Rendering → Emulate CSS media feature.

Тёмная тема — и переменные окупаются

:root {
    --bg: #fff;  --text: #1a1a1a;  --link: #2f6fb0;
}
@media (prefers-color-scheme: dark) {
    :root { --bg: #16181d;  --text: #e6e8eb;  --link: #8ab8ea; }
}
body { background: var(--bg); color: var(--text); }

Медиазапрос меняет только переменные. Остальной CSS не знает, что тем две.

Один CSS, две темы

Лекция 4 Современный CSS · слайды переменные, медиазапросы, анимации
Лекция 4 Современный CSS · слайды переменные, медиазапросы, анимации
:root { color-scheme: light dark; }

А эта строка перекрасит то, до чего CSS не дотянуться: полосы прокрутки и поля ввода.

🥚 На главной странице курса есть пасхалка invert

Картинки: одна строка обязательна

img {
    max-width: 100%;
    height: auto;
}

Без неё фотография шириной 1600 пикселей распирает телефон, и у страницы появляется горизонтальная прокрутка. Второй ингредиент Маркотта — ровно эти две строки.

Не гонять телефону огромный файл

<img src="cat-800.jpg"
     srcset="cat-400.jpg 400w, cat-800.jpg 800w, cat-1600.jpg 1600w"
     sizes="(min-width: 900px) 50vw, 100vw"
     alt="Кот на подоконнике">

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

Нужна другая кадрировка, а не другой размер, — <picture>. Картинка в рамке — object-fit: cover.

min(), max(), clamp()

Долг из лекции 3. Три функции, которые выбирают значение сами:

width: min(100%, 70ch);         /* меньшее из двух */
padding: max(1rem, 3vw);        /* большее из двух */
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);

clamp(минимум, желаемое, максимум) — «не меньше, не больше, а между — по обстоятельствам».

Резиновый заголовок

h1 { font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem); }
Заголовок320px → 24px
Заголовок900px → 38px
Заголовок1600px → 48px

Растёт вместе с окном — плавно, без единого брейкпоинта, и упирается в потолок.

Почему в середине смесь, а не чистый vw

font-size: 5vw зависит только от окна. Человек увеличил шрифт в настройках браузера — а заголовку всё равно.

1rem + 2.5vw растёт и от окна, и от настроек. Та же доступность, что в лекции 3, — только уже в формуле.

Контейнер в одну строку

раньше

.wrap {
  max-width: 70ch;
  margin: 0 auto;
  padding: 0 1rem;
}

сейчас

.wrap {
  width: min(100% - 2rem, 70ch);
  margin-inline: auto;
}

На телефоне — вся ширина минус поля, на компьютере — комфортные 70 знаков. Без медиазапроса.

Чего медиазапрос не решает

Карточка должна быть горизонтальной, если ей просторно, и вертикальной, если тесно. Медиазапрос спрашивает ширину окна.

А на одном и том же экране шириной 1400 карточка может стоять и в широкой колонке, и в узком сайдбаре. Окно одно — места разные.

Container queries

.sidebar, .main { container-type: inline-size; }

@container (min-width: 380px) {
    .card { grid-template-columns: 110px 1fr; }
}

Родитель объявляет себя контейнером, и компонент спрашивает ширину своего места, а не окна.

Главная новинка CSS последних лет: во всех браузерах с 2023 года.

Одна карточка, два места

сайдбар

🎨
Лекция 4Современный CSS

основная колонка

🎨
Лекция 4Современный CSS

Окно одно на обе. Раскладка — своя у каждой.

карточка

Правило выбора

Макет страницы — медиазапрос.
Компонент — контейнерный запрос.

Сколько колонок у страницы и прятать ли меню — решает окно. Как выглядит карточка — решает место, куда её поставили.

Бонус: единицы cqi — проценты от ширины контейнера, как vw, только от своего места.

Починили кружку из лекции 2

CSS IS
AWESOME
лекция 2: font-size: 66px
CSS IS
AWESOME
сегодня: font-size: 21cqi
.mug      { container-type: inline-size; }
.mug span { font-size: 21cqi; }   /* 21 % ширины кружки */

Адаптивность в пяти строках

Часть 3 · 10 минут transform

Сдвинуть, повернуть, увеличить

Мы уже встречались

.modal {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

Способ центрирования №4 из лекции 3. Тогда — без теории. Сегодня — что ещё умеет это свойство.

Четыре функции

translate(18px, -10px)
rotate(30deg)
scale(1.3)
skewX(-20deg)

Пунктир — где элемент стоит на самом деле. Об этом следующий слайд.

Главное свойство трансформации

Она не влияет на поток. Элемент рисуется в другом месте, повёрнутым или увеличенным, а в раскладке занимает прежнее место прежнего размера. Соседи ничего не замечают.

Как position: relative из лекции 3 — сдвиг без последствий. И именно поэтому трансформации так быстро анимируются: об этом в части 4.

transform-origin

.door {
    /* как дверь на петлях */
    transform-origin: left center;
    transform: rotateY(40deg);
}

Точка, вокруг которой вращают и от которой масштабируют. По умолчанию — центр. top left, 50% 100%, 20px 10px — всё можно.

🥚 На главной странице курса: do a barrel roll

Есть ли разница?

Те же функции, другой порядок

.a { transform: rotate(45deg) translateX(80px); }
.b { transform: translateX(80px) rotate(45deg); }

Повернуть и сдвинуть — или сдвинуть и повернуть. От перестановки слагаемых…?

Проверяем

rotate(45deg) translateX(80px)
translateX(80px) rotate(45deg)

Разница есть. Поворот поворачивает и оси: после rotate «вправо» смотрит по диагонали.

Отдельные свойства

.icon       { rotate: 15deg; }
.icon:hover { scale: 1.2; }     /* rotate не сбился */

С 2022 года translate, rotate и scale есть как самостоятельные свойства. С одним transform на :hover пришлось бы переписывать всю строку, включая поворот.

И в трёх измерениях

наведите мышь
обратная сторона
.flip     { perspective: 800px; }
.flip .in { transform-style: preserve-3d; }
.face     { backface-visibility: hidden; }
.flip:hover .in { transform: rotateY(180deg); }

Напоминание из лекции 3

Любой transform — даже translateZ(0), который ничего не двигает, — создаёт элементу контекст наложения и становится точкой отсчёта для position: fixed внутри.

Повесили трансформацию на обёртку страницы — и шапка перестала быть прилипшей. Теперь вы знаете почему.

Карточка приподнимается

🎨
Лекция 4наведите на меня
.card:hover {
    translate: 0 -10px;
    box-shadow: 0 16px 32px
        rgb(30 60 100 / 0.28);
}

Работает. Но прыгает рывком — это чинит часть 4.

карточка

Часть 4 · 13 минут transition

Не рывком, а плавно

Кнопка из лекции 2

рывок
0.3 секунды

Наведите на обе. Разницу видит каждый, даже если не может её назвать: первая переключается, вторая меняется.

Одна строка

.button {
    background: #4b86c2;
    transition: background-color 0.3s ease-out;
}
.button:hover { background: #c0392b; }

Браузер сам достроит все промежуточные цвета между синим и красным.

Из чего состоит

propertyчто плавно менять
durationсколько длится
timing-functionкак разгоняться
delayсколько подождать
/* несколько свойств — через запятую */
transition: transform 0.2s ease-out,
            opacity   0.2s ease-out 0.1s;

А если написать
только в :hover?

Переход переехал

.button       { background: #4b86c2; }
.button:hover {
    background: #c0392b;
    transition: background-color 0.6s;
}

Что будет, когда мышь наведут — и когда уведут?

Проверяем

плавно туда и обратно
туда плавно, обратно рывком
Туда плавно, обратно рывком. Мышь ушла — действует правило без :hover, а в нём перехода нет.

transition: all — не надо

🙅
transition: all — через полгода поедет и чей-то width
👉
transition: transform 0.3s — ровно то, что задумано

all анимирует и случайное, и дорогое. Перечисляйте явно.

Функции времени

linear
ровно
ease
по умолчанию
ease-in
разгон
ease-out
торможение
ease-in-out
и то и другое

По горизонтали — время, по вертикали — пройденный путь.

Какую когда

ease-outпоявление: влетает быстро, мягко останавливается
ease-inисчезновение: разгоняется и уходит
ease-in-outдвижение из точки в точку на экране
linearтолько вращение и полоски прогресса

В реальном мире ничего не движется linear — поэтому глаз считает его механическим.

Свою кривую — четырьмя числами

/* ease — это просто cubic-bezier(0.25, 0.1, 0.25, 1) */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);

Число больше единицы — и элемент перелетает цель и возвращается, как на пружинке.

И steps(5) — не плавно, а пятью скачками. Пригодится в части 5.

Кривые — дома

cubic-bezier.com Леа Веру: тянете две точки мышью, смотрите, как едут квадратики, копируете готовые четыре числа.

QR-код со ссылкой на сайт cubic-bezier.com cubic-bezier.com редактор кривых

Такой же редактор встроен в DevTools: щёлкните по иконке кривой рядом с ease в панели Styles.

Lea Verou, cubic-bezier.com

Что вообще можно анимировать

Всё, у чего есть промежуточные значения: длины, цвета, числа, тени, трансформации. Между 10px и 20px есть 15px, между синим и красным — фиолетовый.

А между display: none и block ничего нет — такое свойство переключается сразу.

🥚 На главной странице курса: kerning. Там letter-spacing меняется рывком — вы уже знаете, как исправить

Две старые боли и новые ответы

появиться из none

.popup {
  transition: opacity .3s,
    display .3s allow-discrete;
}
@starting-style {
  .popup { opacity: 0; }
}

до height: auto

.acc {
  display: grid;
  grid-template-rows: 0fr;
  transition:
    grid-template-rows .3s;
}
.acc.open {
  grid-template-rows: 1fr;
}

Как браузер рисует кадр

Стилькакие правила
→
Раскладкагде и какого размера
→
Отрисовкапиксели каждого слоя
→
Композициясложить слои

width, height, left, top, margin

Возврат к лекции 1. Анимация left запускает всю цепочку — шестьдесят раз в секунду.

А transform и opacity — нет

Стиль—
→
Раскладкане нужна
→
Отрисовкане нужна
→
Композициявидеокарта, отдельный поток

transform, opacity

Трансформация не влияет на поток — значит, раскладку пересчитывать не нужно. Браузер двигает уже готовую картинку слоя.

Анимируйте
transform и opacity Двигать — translate, а не left. Менять размер — scale, а не width. Прятать — opacity

will-change — с осторожностью

.drawer { will-change: transform; }

Подсказка: «это скоро будет двигаться, подготовь слой заранее». Помогает, если анимация дёргается в самом начале.

Но это тот самый will-change из лекции 3: создаёт контекст наложения. Ставить точечно, а не на всё подряд.

Теперь карточка — плавно

🎨
Лекция 4наведите на меня
.card {
    transition: translate 0.25s ease-out,
                box-shadow 0.25s ease-out;
}

Анимируем translate, а не top. Перечисляем явно, а не all. Пишем на основном правиле, а не в :hover.

карточка

Часть 5 · 17 минут Анимации
@keyframes

Которые идут сами

1995

Первые анимации веба

🚧 UNDER CONSTRUCTION 🚧
Добро пожаловать на мою домашнюю страницу!!! Вы посетитель №000137

NEW! Мои любимые ссылки

<blink>, <marquee> и GIF «сайт строится». Теги давно выкинули — здесь всё на @keyframes.

Переход против анимации

transition

из A в B, по событию, один раз. Нужен триггер: наведение, класс, фокус

animation

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

«При наведении стать другим» — переход. «Крутиться, пока грузится» — анимация.

Сценарий и подключение

@keyframes spin {
    from { rotate: 0deg; }
    to   { rotate: 360deg; }
}

.spinner { animation: spin 1s linear infinite; }

Сценарий объявляется отдельно и получает имя. Подключается — к любому количеству элементов.

Кадров — сколько угодно

@keyframes bounce {
    0%, 100% { translate: 0 0; }
    40%      { translate: 0 -40px; }
    60%      { translate: 0 -20px; }
}

Проценты — моменты времени. Между ними браузер достраивает всё сам, с той функцией времени, которую вы указали.

Восемь свойств

nameкакой сценарий
durationдлина одного круга
timing-functionкак разгоняться
delayпауза до старта
iteration-countсколько раз, infinite
directionвперёд, назад, туда-обратно
fill-modeчто до и после
play-stateпауза

Обычно — одной строкой. Первое время в ней — длительность, второе — задержка.

Почему он
прыгает назад?

Уведомление выезжает

@keyframes slide-in {
    from { translate: -100% 0; }
    to   { translate: 0 0; }
}
.toast {
    translate: -100% 0;
    animation: slide-in 1s ease-out;
}

Секунду уведомление плавно выезжает. А в конце — прыгает обратно за край экрана. Почему?

Проверяем

Анимация закончилась — и перестала действовать. Элемент вернулся к своим обычным стилям, а там записано «за краем».
animation: slide-in 1s ease-out forwards;

fill-mode: forwards — «оставь последний кадр». backwards — показать первый кадр ещё во время delay, both — и то и другое.

Туда-обратно без второго сценария

@keyframes breathe {
    from { opacity: 1;   scale: 1; }
    to   { opacity: 0.3; scale: 0.7; }
}
.dot { animation: breathe 1s ease-in-out infinite alternate; }

alternate проигрывает каждый второй круг задом наперёд. Вместе с infinite получается дыхание.

Три анимации с каждого сайта

В ЭФИРЕ

rotate · linear · infinite

scale + opacity · alternate

background-position

Спиннер, индикатор и скелетон загрузки. Каждый — три-пять строк CSS, ни одной строки JavaScript.

Скелетон изнутри

.skeleton {
    background: linear-gradient(90deg,
        #eceff3 25%, #f7f8fa 50%, #eceff3 75%);
    background-size: 300% 100%;
    animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer {
    from { background-position: 100% 0; }
    to   { background-position: 0 0; }
}

Градиент втрое шире блока и едет сквозь него — блик.

Переменные + анимации

.item { animation: rise 0.4s ease-out both;
        animation-delay: calc(var(--i) * 120ms); }
<li style="--i: 0">  <li style="--i: 1">  <li style="--i: 2"> ...

Одно правило на всех, задержка у каждого своя. Часть 1 замкнулась.

steps(): как в мультфильме

.sprite {
    width: 64px;
    background: url(run.png);   /* восемь кадров в ряд */
    animation: run 0.8s steps(8) infinite;
}
@keyframes run { to { background-position: -512px 0; } }

Не плавно, а восемью скачками — ровно по кадру спрайта. Так до сих пор делают анимированные иконки и пиксельную графику.

🥚 На главной странице курса: blink — там тоже steps()

Когда впервые узнал про @keyframes

ЛЕКЦИЯ 4 ⭐

Современный CSS

ЖМИ СЮДА

Анимация должна
что-то объяснять Откуда элемент пришёл, куда ушёл, что сейчас происходит. Если она ничего не объясняет — она мешает

2013

Анимация может навредить

iOS 7 принесла параллакс и масштабирующиеся переходы между экранами. Часть пользователей начало укачивать — буквально, до тошноты и головокружения.

Через месяц Apple добавила настройку «Уменьшение движения». Сейчас она есть в каждой системе, и веб умеет о ней спросить.

prefers-reduced-motion

/* анимации — только тем, кто не просил их убрать */
@media (prefers-reduced-motion: no-preference) {
    .card { animation: rise 0.4s ease-out both; }
}

Движение ради красоты — выключаемое. Движение, которое несёт смысл, — заменяемое: вместо полёта через экран — короткое затухание.

Анимации на этих слайдах, кстати, тоже её уважают.

Замедлить и поймать кадр

DevTools → More tools → Animations. Все анимации страницы на временной шкале: замедлить до 10 %, перемотать, остановить на нужном кадре и поправить значения прямо там.

Четвёртый инструмент DevTools в курсе. Спиннер, который «как-то дёргается», на скорости 10 % показывает виноватого за пять секунд.

Что появилось за последние годы

animation-timelineанимация по прокрутке: индикатор чтения без JavaScript
View Transitionsплавный переход между состояниями страницы
@propertyпеременная с типом — её можно анимировать
.progress { animation: grow linear; animation-timeline: scroll(); }

Поддержка ещё неровная — перед использованием смотрите caniuse.com.

Карточка загружается

🎨
Лекция 4Современный CSS
🎨
Лекция 4Современный CSS
@media (prefers-
  reduced-motion:
  no-preference) {
  .loading * {
    animation:
      shimmer 1.4s
      linear infinite;
  }
}

Пока данных нет — скелетон. При reduce — просто серый.

карточка

Часть 6 · 7 минутЧто дальше

Собираем карточку

Не пять тем,
а пять слоёв
одного компонента Переменные — чем покрасить. Контейнер — как разложить. Трансформация — куда сдвинуть. Переход — как быстро. Анимация — что делать, пока ждём

Что освоили

Современный CSS не кончается

:has()родитель, который знает о детях
color-mix(), oklch()цвета, которые можно вычислять
light-dark()две темы в одном значении
@scopeстили, ограниченные областью
вложенностьправила внутри правил — лекция 5
@layerкаскадные слои — лекция 5

Всё это уже работает в браузерах. В курс не влезло — но теперь вы знаете, что искать.

Упражнение на вечер

Не ЛР, а десять минут: откройте свою страницу из ЛР 2 в режиме устройства на 375 пикселях.

Что почитать

web.dev/learn/cssразделы про адаптивность и анимации
MDNподробно и точно, по каждому свойству
Докапо-русски и человеческим языком
QR-код со ссылкой на раздел CSS справочника «Дока» doka.guide/css Справочник по CSS

Лекция 5. Организация CSS

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

Вопросы?

Слайды, демо и игра — в репозитории курса