Веб-программирование
Веб-программирование · лекция 4 из 16
Научились ставить элементы на места. Сегодня учим страницу подстраиваться и двигаться.
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
«Адаптивность без единого медиазапроса». Сетка сама решает, сколько колонок влезет.
Хорошо для галереи. Но у страницы есть шапка, меню, типографика и картинки — и сами по себе они подстраиваться не умеют.
И какого размера там был текст
У половины зала страница из ЛР на телефоне выглядит как уменьшенная копия компьютерной: всё на месте, но читать можно только с лупой.
Это не ошибка в вашем CSS. Это решение браузера, и сегодня мы узнаем, почему он так решил и как его переубедить.
Крупный текст, одна колонка, кнопку видно сразу.
Читать дальшеПривет! Это моя страница. Здесь крупный текст, одна колонка и кнопку видно сразу.
Ещё немного текста мелким шрифтом, который никто не прочитает без лупы.
clamp(), container queriestransform — сдвинуть, повернуть, увеличитьtransition — сделать это плавно@keyframes — анимации, которые идут самиПять тем, и все пять сойдутся в одной маленькой карточке.
Один раз объявить
.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 — две
разные переменные. Обычные свойства так себя не ведут.
color.
Значит, на них работает всё из лекции 2: каскад, наследование, специфичность.
:rootвидно везде: переменная наследуется от корня всем потомкам
видно только ему и его потомкам: у каждого блока — свои значения
.card { --accent: #4b86c2; }
.card.warm { --accent: #d68910; } /* только у тёплых карточек */
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
$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
Один сайт на все экраны
2007
Выходит iPhone. Экран — 320 точек в ширину. Сайты того времени свёрстаны на 960 пикселей, потому что «у всех мониторы 1024».
Первая реакция индустрии — сделать второй сайт. Отдельная
мобильная версия на m.site.ru: другая разметка, другие
стили, и вечно отстающая от основной.
2010
Итан Маркотт в статье для A List Apart называет подход и собирает его из трёх ингредиентов:
Одна разметка, один 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
Люк Вроблевски, дизайнер интерфейсов, предлагает проектировать сначала для телефона. Не потому что телефонов больше, а потому что маленький экран заставляет решить главное: что на странице действительно нужно.
Начните с важного — и широкий экран станет вопросом «что ещё добавить».
Luke Wroblewski, «Mobile First», A Book Apart, 2011.nav { display: flex; }
@media (max-width: 600px) {
.nav { display: none; }
}
.nav { display: none; }
@media (min-width: 600px) {
.nav { display: flex; }
}
Сначала узкий экран, потом min-width добавляет. Не
наоборот.
desktop first · max-width по убыванию
mobile first · min-width по возрастанию
Слева направо — ширина окна. В первом случае узкий экран отменяет широкий, во втором широкий добавляет к узкому.
.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; }
}
.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 — это просто «не мешай потоку, пока экран узкий».
.col { width: 100%; }
@media (min-width: 900px) { .col { width: 33%; } }
@media (min-width: 600px) { .col { width: 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) { ... }
Не под модели телефонов — их сотни, и через год будут другие. Тяните окно, пока макет не станет некрасивым: там, где ломается контент, и брейкпоинт.
375, 390, 393… — устаревает с каждым новым телефоном576 768 992 1200640 768 1024 1280Числа фреймворков — нормальная отправная точка, если своих причин нет. Но причина всегда в контенте: у фреймворка просто кто-то уже подумал за вас.
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.
Медиазапрос спрашивает не про размер, а про обстоятельства.
:hoverНавести пальцем нельзя, поэтому телефон считает наведением тап — и оставляет его «наведённым», пока не тапнешь в другое место. Кнопка так и висит красной.
@media (hover: hover) {
.card:hover { translate: 0 -4px; }
}
Эффект включается только там, где им правда можно пользоваться.
Человек один раз выбирает в настройках системы — и каждый сайт, который спрашивает, узнаёт ответ. Проверить без смены настроек: 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 не знает, что тем две.
: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); }
Растёт вместе с окном — плавно, без единого брейкпоинта, и упирается в потолок.
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 карточка может стоять и в широкой колонке, и в узком сайдбаре. Окно одно — места разные.
.sidebar, .main { container-type: inline-size; }
@container (min-width: 380px) {
.card { grid-template-columns: 110px 1fr; }
}
Родитель объявляет себя контейнером, и компонент спрашивает ширину своего места, а не окна.
Главная новинка CSS последних лет: во всех браузерах с 2023 года.
сайдбар
основная колонка
Окно одно на обе. Раскладка — своя у каждой.
Сколько колонок у страницы и прятать ли меню — решает окно. Как выглядит карточка — решает место, куда её поставили.
Бонус: единицы cqi — проценты от ширины контейнера, как
vw, только от своего места.
font-size: 66pxfont-size: 21cqi.mug { container-type: inline-size; }
.mug span { font-size: 21cqi; } /* 21 % ширины кружки */
<meta name="viewport"> — без неё не работает ничегоmin-width по возрастаниюclamp() и min() вместо лишних медиазапросовtransform
Сдвинуть, повернуть, увеличить
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Способ центрирования №4 из лекции 3. Тогда — без теории. Сегодня — что ещё умеет это свойство.
Пунктир — где элемент стоит на самом деле. Об этом следующий слайд.
Она не влияет на поток. Элемент рисуется в другом месте, повёрнутым или увеличенным, а в раскладке занимает прежнее место прежнего размера. Соседи ничего не замечают.
Как 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 «вправо» смотрит по диагонали.
.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); }
Любой transform — даже translateZ(0), который
ничего не двигает, — создаёт элементу контекст наложения и
становится точкой отсчёта для position: fixed внутри.
Повесили трансформацию на обёртку страницы — и шапка перестала быть прилипшей. Теперь вы знаете почему.
.card:hover {
translate: 0 -10px;
box-shadow: 0 16px 32px
rgb(30 60 100 / 0.28);
}
Работает. Но прыгает рывком — это чинит часть 4.
transition
Не рывком, а плавно
Наведите на обе. Разницу видит каждый, даже если не может её назвать: первая переключается, вторая меняется.
.button {
background: #4b86c2;
transition: background-color 0.3s ease-out;
}
.button:hover { background: #c0392b; }
Браузер сам достроит все промежуточные цвета между синим и красным.
/* несколько свойств — через запятую */
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 — через полгода поедет и чей-то widthtransition: transform 0.3s — ровно то, что задумано
all анимирует и случайное, и дорогое. Перечисляйте явно.
По горизонтали — время, по вертикали — пройденный путь.
В реальном мире ничего не движется 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 Леа Веру: тянете две точки мышью, смотрите, как едут квадратики, копируете готовые четыре числа.
Такой же редактор встроен в DevTools: щёлкните по иконке кривой рядом
с ease в панели Styles.
Всё, у чего есть промежуточные значения: длины, цвета, числа,
тени, трансформации. Между 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: создаёт контекст
наложения. Ставить точечно, а не на всё подряд.
.card {
transition: translate 0.25s ease-out,
box-shadow 0.25s ease-out;
}
Анимируем translate, а не top. Перечисляем
явно, а не all. Пишем на основном правиле, а не в
:hover.
@keyframes
Которые идут сами
1995
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; }
}
Проценты — моменты времени. Между ними браузер достраивает всё сам, с той функцией времени, которую вы указали.
infiniteОбычно — одной строкой. Первое время в ней — длительность, второе — задержка.
@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()
@keyframes2013
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 % показывает виноватого за пять секунд.
.progress { animation: grow linear; animation-timeline: scroll(); }
Поддержка ещё неровная — перед использованием смотрите caniuse.com.
@media (prefers-
reduced-motion:
no-preference) {
.loading * {
animation:
shimmer 1.4s
linear infinite;
}
}
Пока данных нет — скелетон. При reduce — просто серый.
Собираем карточку
clamp(), контейнерыtransform — движение вне потокаtransition — из A в B плавно и дёшево@keyframes — сценарии, и с уважением к настройкамВсё это уже работает в браузерах. В курс не влезло — но теперь вы знаете, что искать.
Не ЛР, а десять минут: откройте свою страницу из ЛР 2 в режиме устройства на 375 пикселях.
<meta name="viewport">?@layer — уже без препроцессораСегодня научились писать современный CSS. Дальше — как писать его много и не запутаться.
Слайды, демо и игра — в репозитории курса