Анимация элементов в CSS. Часть 2: управление анимацией
Чтение — около 7 минут· без учёта практики
В первой части урока мы описали ключевые кадры и задали анимации длительность, повторы, задержку и скорость. Теперь разберём, как управлять её ходом и что происходит с элементом до и после анимации.
Пауза: animation-play-state
Свойство
animation-play-state
задаёт состояние анимации: running — проигрывается (по умолчанию), paused — приостановлена.
После паузы анимация продолжается с того же места.
Приостановка и запуск анимации: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Приостановка и запуск анимации</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<input type="checkbox" id="pause">
<label for="pause">Приостановить движение</label>
<div class="test">Движение</div>
</body>
</html>
style.css
.test {
width: 100px;
height: 100px;
color: white;
background: green;
position: relative;
animation-name: iliketomoveit;
animation-duration: 5s;
animation-iteration-count: infinite;
animation-play-state: running;
}
#pause:checked ~ .test,
.test:hover {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.test { animation: none; }
}
@keyframes iliketomoveit {
0% {
left: 0px;
}
25% {
left: min(400px, calc(100vw - 120px));
}
75% {
left: min(200px, calc(50vw - 60px));
}
100% {
left: 0px;
}
}
Отметьте флажок «Приостановить движение»: правило #pause:checked ~ .test
задаёт animation-play-state: paused. Флажок доступен с клавиатуры и на сенсорном экране;
пауза сохраняется, пока вы сами не снимете отметку. Попробуйте нажать Tab, затем Пробел.
Дополнительно пауза включается при наведении через :hover, но для бегущей строки или карусели
одного наведения недостаточно: посетитель должен иметь возможность остановить движение и продолжить работу
с остальной страницей. При настройке уменьшения движения анимация в примере отключается;
подробнее разберём это ниже.
Направление: animation-direction
Свойство
animation-direction
определяет, в каком направлении проигрываются циклы. Для нескольких анимаций значения перечисляют через запятую.
| Значение | Описание |
|---|---|
normal |
Каждый цикл идёт от 0% к 100%, затем анимация сбрасывается в начало. Значение по умолчанию. |
reverse |
Каждый цикл идёт в обратную сторону — от 100% к 0%. |
alternate |
Нечётные циклы (1, 3, 5…) — вперёд, чётные (2, 4, 6…) — назад: «туда и обратно» без рывка в начало. |
alternate-reverse |
Нечётные циклы — назад, чётные — вперёд. |
Направление анимации в CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Направление анимации в CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="test">normal</div>
<div class="test2">reverse</div>
<div class="test3">alternate</div>
<div class="test4">alternate-<br>reverse</div>
</body>
</html>
style.css
div {
display: inline-block;
color: white;
background-color: green;
padding: 15px;
width: 75px;
height: 75px;
border-radius: 60px;
position: relative;
animation-iteration-count: infinite;
animation-duration: 1.5s;
animation-name: iliketomoveit;
}
.test {
animation-direction: normal;
}
.test2 {
animation-direction: reverse;
}
.test3 {
animation-direction: alternate;
}
.test4 {
animation-direction: alternate-reverse;
}
@keyframes iliketomoveit {
0% {
top: 0px;
}
25% {
top: 180px;
background: red;
}
75% {
top: 90px;
}
100% {
top: 0px;
background: black;
}
}
Вид до и после анимации: animation-fill-mode
Пока анимация не началась (во время задержки) и после того как она закончилась, элемент по умолчанию выглядит так, как
задано его обычными стилями. Свойство
animation-fill-mode
позволяет «растянуть» ключевые кадры на это время:
| Значение | Описание |
|---|---|
none |
До и после анимации стили кадров не применяются. Значение по умолчанию. |
forwards |
После окончания элемент сохраняет стиль последнего проигранного кадра. |
backwards |
Во время задержки элемент уже получает стиль первого кадра. |
both |
Действуют оба правила: первый кадр — во время задержки, последний — после окончания. |
Какой кадр считается последним и первым, зависит от направления и числа повторов. При normal последний
кадр после целого числа циклов — 100%. При reverse — 0%. При
alternate последний кадр — 100% после нечётного числа циклов и 0% после
чётного, а при alternate-reverse — наоборот. Первый кадр при normal и
alternate — 0%, при reverse и alternate-reverse —
100%. При дробном числе повторов после окончания сохраняется промежуточное состояние: например,
normal с 1.5 цикла и forwards оставляет состояние на отметке 50%.
Свойство animation-fill-mode в CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Свойство animation-fill-mode в CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<br><br><br>
<div class="test">none</div>
<div class="test2">forwards</div>
<div class="test3">backwards</div>
<div class="test4">both</div>
</body>
</html>
style.css
div {
width: 100px;
height: 100px;
color: white;
display: inline-block;
background: green;
position: relative;
animation-name: iliketomoveit;
animation-duration: 5s;
animation-delay: 2s;
}
.test {
animation-fill-mode: none;
}
.test2 {
animation-fill-mode: forwards;
}
.test3 {
animation-fill-mode: backwards;
}
.test4 {
animation-fill-mode: both;
}
@keyframes iliketomoveit {
0% {
background: yellow;
color: #242424;
}
100% {
background: red;
color: white;
}
}
В примере анимация длится 5 секунд и меняет цвет фона с жёлтого на красный, у неё задержка 2 секунды. Первый блок
(none) во время задержки и после анимации — обычного зелёного цвета. Второй (forwards) после
окончания остаётся красным. Третий (backwards) во время задержки уже жёлтый. Четвёртый
(both) жёлтый во время задержки и красный после окончания.
Сокращённое свойство animation
Свойство
animation
задаёт все настройки одной записью. Повторим их:
| Свойство | Значения |
|---|---|
animation-name |
Имя из @keyframes или none. |
animation-duration |
Время одного цикла. |
animation-timing-function |
ease, linear, ease-in, ease-out, ease-in-out,
cubic-bezier(), steps(), step-start, step-end,
linear().
|
animation-delay |
Время задержки. |
animation-iteration-count |
Число или infinite. |
animation-direction |
normal, reverse, alternate, alternate-reverse. |
animation-fill-mode |
none, forwards, backwards, both. |
animation-play-state |
running, paused. |
Значения в animation можно писать в любом порядке, кроме двух правил. Первое время — длительность, второе
— задержка. А имя анимации лучше ставить первым или последним и не называть анимацию так же, как ключевые слова
(ease, infinite, forwards), иначе браузер примет имя за значение другого
свойства. Не забывайте длительность: без неё анимация не будет видна.
animation: rise 0.6s ease-out 0.2s both;
/* имя, длительность, функция времени, задержка, fill-mode */
Пример анимации на CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Пример анимации на CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="test"></div>
</body>
</html>
style.css
.test {
width: 100px;
height: 100px;
position: relative;
animation: iliketomoveit 4s steps(3, start) 500ms infinite reverse;
}
@keyframes iliketomoveit {
0% {
left: 0px;
}
50% {
background-color: plum;
left: min(500px, calc(100vw - 120px));
border-radius: 50px;
}
100% {
background-color: green;
left: 0px;
}
}
В этом примере анимация iliketomoveit длится 4 секунды, идёт шагами steps(3, start) — по три
шага на каждый промежуток между кадрами, — начинается через 500 миллисекунд, повторяется бесконечно и проигрывается в
обратную сторону (reverse). Она сдвигает элемент, меняет цвет фона и скругление углов.
А вот индикатор загрузки: одна анимация up назначена нескольким точкам с разной задержкой — от 1 до 1,8
секунды, — поэтому они подпрыгивают по очереди. Каждая анимация длится секунду, идёт с одинаковой скоростью
(linear) и повторяется бесконечно; точки выбраны псевдоклассом :nth-child().
Анимация загрузки на CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Анимация загрузки на CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</body>
</html>
style.css
body {
margin: 0;
padding: 0;
}
.container {
width: 100px;
padding-top: 100px;
margin: 0 auto;
}
div > div {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 auto;
border-radius: 50px;
}
.item:nth-child(1) {
background: orange;
animation: up 1s linear 1s infinite;
}
.item:nth-child(2) {
background: violet;
animation: up 1s linear 1.2s infinite;
}
.item:nth-child(3) {
background: magenta;
animation: up 1s linear 1.4s infinite;
}
.item:nth-child(4) {
background: lightseagreen;
animation: up 1s linear 1.6s infinite;
}
.item:nth-child(5) {
background: forestgreen;
animation: up 1s linear 1.8s infinite;
}
@keyframes up {
0%,
100% {
transform: translateY(-15px);
}
50% {
transform: translate(5px, 0);
}
}
Уменьшение движения
Как и переходы, анимации отключают для посетителей, которые включили в системе уменьшение движения. Обычно это делают одним правилом в конце таблицы стилей:
@media (prefers-reduced-motion: reduce) {
.intro > * {
animation: none; /* приветствие видно сразу, без появления */
}
}
Главное — после отключения содержимое должно остаться на месте и видимым. Если элемент виден только благодаря анимации
(например, у него opacity: 0, а проявляет его forwards), при animation: none он
исчезнет. Поэтому обычные стили описывают конечный вид, а анимация лишь ведёт к нему.
Библиотека Animate.css
Этот раздел необязательный: он знакомит с готовыми анимациями, но для курса достаточно своих. Сложные анимации создавать долго, и иногда проще взять готовые. Библиотека Animate.css — это файл стилей с набором ключевых кадров и классов для их запуска: появление и исчезновение, повороты, увеличение, эффекты привлечения внимания и многое другое. Все анимации можно посмотреть на сайте проекта, а исходный код — в репозитории на GitHub.
Файл библиотеки кладут в папку сайта и подключают элементом <link>, а затем добавляют элементу
классы. В четвёртой версии библиотеки имена классов начинаются с animate__:
<link rel="stylesheet" href="css/animate.css">
…
<img src="photo.png" alt="…" class="animate__animated animate__fadeIn">
-
animate__animatedзадаёт общие настройки — длительность 1 секунда иanimation-fill-mode: both; animate__fadeIn— имя анимации (здесь — плавное появление черезopacity);animate__infinite— бесконечное повторение.
Пример ниже написан для третьей версии библиотеки, где те же классы назывались без префикса: animated,
fadeIn, infinite. Такие имена до сих пор встречаются в старых проектах. Обычно классы
анимации добавляет JavaScript в ответ на действия посетителя; здесь они записаны прямо в HTML. Animate.css сама
учитывает prefers-reduced-motion, начиная с версии 3.7.0. В примере сохранена версия 3.5.2, поэтому
правило уменьшения движения добавлено отдельно в конце её стилей.
Анимация с использованием библиотеки Animate.CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Анимация с использованием библиотеки Animate.CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<img src="assets/fair.png" alt="Плюшевый мишка" width="200" height="200" class="animated infinite fadeIn">
</body>
</html>
style.css
@charset "UTF-8";
/*!
* animate.css -http://daneden.me/animate
* Version - 3.5.2
* Licensed under the MIT license - http://opensource.org/licenses/MIT
*
* Copyright (c) 2017 Daniel Eden
*/
.animated {
animation-duration: 1s;
animation-fill-mode: both;
}
.animated.infinite {
animation-iteration-count: infinite;
}
.animated.hinge {
animation-duration: 2s;
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
animation-duration: 0.75s;
}
@keyframes bounce {
from,
20%,
53%,
80%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translate3d(0, 0, 0);
}
40%,
43% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -30px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -15px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
.bounce {
animation-name: bounce;
transform-origin: center bottom;
}
@keyframes flash {
from,
50%,
to {
opacity: 1;
}
25%,
75% {
opacity: 0;
}
}
.flash {
animation-name: flash;
}
/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
@keyframes pulse {
from {
transform: scale3d(1, 1, 1);
}
50% {
transform: scale3d(1.05, 1.05, 1.05);
}
to {
transform: scale3d(1, 1, 1);
}
}
.pulse {
animation-name: pulse;
}
@keyframes rubberBand {
from {
transform: scale3d(1, 1, 1);
}
30% {
transform: scale3d(1.25, 0.75, 1);
}
40% {
transform: scale3d(0.75, 1.25, 1);
}
50% {
transform: scale3d(1.15, 0.85, 1);
}
65% {
transform: scale3d(0.95, 1.05, 1);
}
75% {
transform: scale3d(1.05, 0.95, 1);
}
to {
transform: scale3d(1, 1, 1);
}
}
.rubberBand {
animation-name: rubberBand;
}
@keyframes shake {
from,
to {
transform: translate3d(0, 0, 0);
}
10%,
30%,
50%,
70%,
90% {
transform: translate3d(-10px, 0, 0);
}
20%,
40%,
60%,
80% {
transform: translate3d(10px, 0, 0);
}
}
.shake {
animation-name: shake;
}
@keyframes headShake {
0% {
transform: translateX(0);
}
6.5% {
transform: translateX(-6px) rotateY(-9deg);
}
18.5% {
transform: translateX(5px) rotateY(7deg);
}
31.5% {
transform: translateX(-3px) rotateY(-5deg);
}
43.5% {
transform: translateX(2px) rotateY(3deg);
}
50% {
transform: translateX(0);
}
}
.headShake {
animation-timing-function: ease-in-out;
animation-name: headShake;
}
@keyframes swing {
20% {
transform: rotate3d(0, 0, 1, 15deg);
}
40% {
transform: rotate3d(0, 0, 1, -10deg);
}
60% {
transform: rotate3d(0, 0, 1, 5deg);
}
80% {
transform: rotate3d(0, 0, 1, -5deg);
}
to {
transform: rotate3d(0, 0, 1, 0deg);
}
}
.swing {
transform-origin: top center;
animation-name: swing;
}
@keyframes tada {
from {
transform: scale3d(1, 1, 1);
}
10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
transform: scale3d(1, 1, 1);
}
}
.tada {
animation-name: tada;
}
/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
@keyframes wobble {
from {
transform: none;
}
15% {
transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
}
30% {
transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
}
45% {
transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
}
60% {
transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
}
75% {
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
}
to {
transform: none;
}
}
.wobble {
animation-name: wobble;
}
@keyframes jello {
from,
11.1%,
to {
transform: none;
}
22.2% {
transform: skewX(-12.5deg) skewY(-12.5deg);
}
33.3% {
transform: skewX(6.25deg) skewY(6.25deg);
}
44.4% {
transform: skewX(-3.125deg) skewY(-3.125deg);
}
55.5% {
transform: skewX(1.5625deg) skewY(1.5625deg);
}
66.6% {
transform: skewX(-0.78125deg) skewY(-0.78125deg);
}
77.7% {
transform: skewX(0.390625deg) skewY(0.390625deg);
}
88.8% {
transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
}
}
.jello {
animation-name: jello;
transform-origin: center;
}
@keyframes bounceIn {
from,
20%,
40%,
60%,
80%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
20% {
transform: scale3d(1.1, 1.1, 1.1);
}
40% {
transform: scale3d(0.9, 0.9, 0.9);
}
60% {
opacity: 1;
transform: scale3d(1.03, 1.03, 1.03);
}
80% {
transform: scale3d(0.97, 0.97, 0.97);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
.bounceIn {
animation-name: bounceIn;
}
@keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, 25px, 0);
}
75% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, 5px, 0);
}
to {
transform: none;
}
}
.bounceInDown {
animation-name: bounceInDown;
}
@keyframes bounceInLeft {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(-3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(25px, 0, 0);
}
75% {
transform: translate3d(-10px, 0, 0);
}
90% {
transform: translate3d(5px, 0, 0);
}
to {
transform: none;
}
}
.bounceInLeft {
animation-name: bounceInLeft;
}
@keyframes bounceInRight {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(-25px, 0, 0);
}
75% {
transform: translate3d(10px, 0, 0);
}
90% {
transform: translate3d(-5px, 0, 0);
}
to {
transform: none;
}
}
.bounceInRight {
animation-name: bounceInRight;
}
@keyframes bounceInUp {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, -20px, 0);
}
75% {
transform: translate3d(0, 10px, 0);
}
90% {
transform: translate3d(0, -5px, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
.bounceInUp {
animation-name: bounceInUp;
}
@keyframes bounceOut {
20% {
transform: scale3d(0.9, 0.9, 0.9);
}
50%,
55% {
opacity: 1;
transform: scale3d(1.1, 1.1, 1.1);
}
to {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
}
.bounceOut {
animation-name: bounceOut;
}
@keyframes bounceOutDown {
20% {
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
transform: translate3d(0, 2000px, 0);
}
}
.bounceOutDown {
animation-name: bounceOutDown;
}
@keyframes bounceOutLeft {
20% {
opacity: 1;
transform: translate3d(20px, 0, 0);
}
to {
opacity: 0;
transform: translate3d(-2000px, 0, 0);
}
}
.bounceOutLeft {
animation-name: bounceOutLeft;
}
@keyframes bounceOutRight {
20% {
opacity: 1;
transform: translate3d(-20px, 0, 0);
}
to {
opacity: 0;
transform: translate3d(2000px, 0, 0);
}
}
.bounceOutRight {
animation-name: bounceOutRight;
}
@keyframes bounceOutUp {
20% {
transform: translate3d(0, -10px, 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 0;
transform: translate3d(0, -2000px, 0);
}
}
.bounceOutUp {
animation-name: bounceOutUp;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fadeIn {
animation-name: fadeIn;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInDown {
animation-name: fadeInDown;
}
@keyframes fadeInDownBig {
from {
opacity: 0;
transform: translate3d(0, -2000px, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInDownBig {
animation-name: fadeInDownBig;
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInLeft {
animation-name: fadeInLeft;
}
@keyframes fadeInLeftBig {
from {
opacity: 0;
transform: translate3d(-2000px, 0, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInLeftBig {
animation-name: fadeInLeftBig;
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInRight {
animation-name: fadeInRight;
}
@keyframes fadeInRightBig {
from {
opacity: 0;
transform: translate3d(2000px, 0, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInRightBig {
animation-name: fadeInRightBig;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInUp {
animation-name: fadeInUp;
}
@keyframes fadeInUpBig {
from {
opacity: 0;
transform: translate3d(0, 2000px, 0);
}
to {
opacity: 1;
transform: none;
}
}
.fadeInUpBig {
animation-name: fadeInUpBig;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fadeOut {
animation-name: fadeOut;
}
@keyframes fadeOutDown {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
}
.fadeOutDown {
animation-name: fadeOutDown;
}
@keyframes fadeOutDownBig {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, 2000px, 0);
}
}
.fadeOutDownBig {
animation-name: fadeOutDownBig;
}
@keyframes fadeOutLeft {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
}
.fadeOutLeft {
animation-name: fadeOutLeft;
}
@keyframes fadeOutLeftBig {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(-2000px, 0, 0);
}
}
.fadeOutLeftBig {
animation-name: fadeOutLeftBig;
}
@keyframes fadeOutRight {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
}
.fadeOutRight {
animation-name: fadeOutRight;
}
@keyframes fadeOutRightBig {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(2000px, 0, 0);
}
}
.fadeOutRightBig {
animation-name: fadeOutRightBig;
}
@keyframes fadeOutUp {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
}
.fadeOutUp {
animation-name: fadeOutUp;
}
@keyframes fadeOutUpBig {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, -2000px, 0);
}
}
.fadeOutUpBig {
animation-name: fadeOutUpBig;
}
@keyframes flip {
from {
transform: perspective(400px) rotate3d(0, 1, 0, -360deg);
animation-timing-function: ease-out;
}
40% {
transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);
animation-timing-function: ease-out;
}
50% {
transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);
animation-timing-function: ease-in;
}
80% {
transform: perspective(400px) scale3d(0.95, 0.95, 0.95);
animation-timing-function: ease-in;
}
to {
transform: perspective(400px);
animation-timing-function: ease-in;
}
}
.animated.flip {
backface-visibility: visible;
animation-name: flip;
}
@keyframes flipInX {
from {
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
animation-timing-function: ease-in;
opacity: 0;
}
40% {
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
animation-timing-function: ease-in;
}
60% {
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
opacity: 1;
}
80% {
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
}
to {
transform: perspective(400px);
}
}
.flipInX {
backface-visibility: visible !important;
animation-name: flipInX;
}
@keyframes flipInY {
from {
transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
animation-timing-function: ease-in;
opacity: 0;
}
40% {
transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
animation-timing-function: ease-in;
}
60% {
transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
opacity: 1;
}
80% {
transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
}
to {
transform: perspective(400px);
}
}
.flipInY {
backface-visibility: visible !important;
animation-name: flipInY;
}
@keyframes flipOutX {
from {
transform: perspective(400px);
}
30% {
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
opacity: 1;
}
to {
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
opacity: 0;
}
}
.flipOutX {
animation-name: flipOutX;
backface-visibility: visible !important;
}
@keyframes flipOutY {
from {
transform: perspective(400px);
}
30% {
transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
opacity: 1;
}
to {
transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
opacity: 0;
}
}
.flipOutY {
backface-visibility: visible !important;
animation-name: flipOutY;
}
@keyframes lightSpeedIn {
from {
transform: translate3d(100%, 0, 0) skewX(-30deg);
opacity: 0;
}
60% {
transform: skewX(20deg);
opacity: 1;
}
80% {
transform: skewX(-5deg);
opacity: 1;
}
to {
transform: none;
opacity: 1;
}
}
.lightSpeedIn {
animation-name: lightSpeedIn;
animation-timing-function: ease-out;
}
@keyframes lightSpeedOut {
from {
opacity: 1;
}
to {
transform: translate3d(100%, 0, 0) skewX(30deg);
opacity: 0;
}
}
.lightSpeedOut {
animation-name: lightSpeedOut;
animation-timing-function: ease-in;
}
@keyframes rotateIn {
from {
transform-origin: center;
transform: rotate3d(0, 0, 1, -200deg);
opacity: 0;
}
to {
transform-origin: center;
transform: none;
opacity: 1;
}
}
.rotateIn {
animation-name: rotateIn;
}
@keyframes rotateInDownLeft {
from {
transform-origin: left bottom;
transform: rotate3d(0, 0, 1, -45deg);
opacity: 0;
}
to {
transform-origin: left bottom;
transform: none;
opacity: 1;
}
}
.rotateInDownLeft {
animation-name: rotateInDownLeft;
}
@keyframes rotateInDownRight {
from {
transform-origin: right bottom;
transform: rotate3d(0, 0, 1, 45deg);
opacity: 0;
}
to {
transform-origin: right bottom;
transform: none;
opacity: 1;
}
}
.rotateInDownRight {
animation-name: rotateInDownRight;
}
@keyframes rotateInUpLeft {
from {
transform-origin: left bottom;
transform: rotate3d(0, 0, 1, 45deg);
opacity: 0;
}
to {
transform-origin: left bottom;
transform: none;
opacity: 1;
}
}
.rotateInUpLeft {
animation-name: rotateInUpLeft;
}
@keyframes rotateInUpRight {
from {
transform-origin: right bottom;
transform: rotate3d(0, 0, 1, -90deg);
opacity: 0;
}
to {
transform-origin: right bottom;
transform: none;
opacity: 1;
}
}
.rotateInUpRight {
animation-name: rotateInUpRight;
}
@keyframes rotateOut {
from {
transform-origin: center;
opacity: 1;
}
to {
transform-origin: center;
transform: rotate3d(0, 0, 1, 200deg);
opacity: 0;
}
}
.rotateOut {
animation-name: rotateOut;
}
@keyframes rotateOutDownLeft {
from {
transform-origin: left bottom;
opacity: 1;
}
to {
transform-origin: left bottom;
transform: rotate3d(0, 0, 1, 45deg);
opacity: 0;
}
}
.rotateOutDownLeft {
animation-name: rotateOutDownLeft;
}
@keyframes rotateOutDownRight {
from {
transform-origin: right bottom;
opacity: 1;
}
to {
transform-origin: right bottom;
transform: rotate3d(0, 0, 1, -45deg);
opacity: 0;
}
}
.rotateOutDownRight {
animation-name: rotateOutDownRight;
}
@keyframes rotateOutUpLeft {
from {
transform-origin: left bottom;
opacity: 1;
}
to {
transform-origin: left bottom;
transform: rotate3d(0, 0, 1, -45deg);
opacity: 0;
}
}
.rotateOutUpLeft {
animation-name: rotateOutUpLeft;
}
@keyframes rotateOutUpRight {
from {
transform-origin: right bottom;
opacity: 1;
}
to {
transform-origin: right bottom;
transform: rotate3d(0, 0, 1, 90deg);
opacity: 0;
}
}
.rotateOutUpRight {
animation-name: rotateOutUpRight;
}
@keyframes hinge {
0% {
transform-origin: top left;
animation-timing-function: ease-in-out;
}
20%,
60% {
transform: rotate3d(0, 0, 1, 80deg);
transform-origin: top left;
animation-timing-function: ease-in-out;
}
40%,
80% {
transform: rotate3d(0, 0, 1, 60deg);
transform-origin: top left;
animation-timing-function: ease-in-out;
opacity: 1;
}
to {
transform: translate3d(0, 700px, 0);
opacity: 0;
}
}
.hinge {
animation-name: hinge;
}
@keyframes jackInTheBox {
from {
opacity: 0;
transform: scale(0.1) rotate(30deg);
transform-origin: center bottom;
}
50% {
transform: rotate(-10deg);
}
70% {
transform: rotate(3deg);
}
to {
opacity: 1;
transform: scale(1);
}
}
.jackInTheBox {
animation-name: jackInTheBox;
}
/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
@keyframes rollIn {
from {
opacity: 0;
transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
}
to {
opacity: 1;
transform: none;
}
}
.rollIn {
animation-name: rollIn;
}
/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
@keyframes rollOut {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
}
}
.rollOut {
animation-name: rollOut;
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
50% {
opacity: 1;
}
}
.zoomIn {
animation-name: zoomIn;
}
@keyframes zoomInDown {
from {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
60% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomInDown {
animation-name: zoomInDown;
}
@keyframes zoomInLeft {
from {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
60% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomInLeft {
animation-name: zoomInLeft;
}
@keyframes zoomInRight {
from {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
60% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomInRight {
animation-name: zoomInRight;
}
@keyframes zoomInUp {
from {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
60% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomInUp {
animation-name: zoomInUp;
}
@keyframes zoomOut {
from {
opacity: 1;
}
50% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
to {
opacity: 0;
}
}
.zoomOut {
animation-name: zoomOut;
}
@keyframes zoomOutDown {
40% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
to {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);
transform-origin: center bottom;
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomOutDown {
animation-name: zoomOutDown;
}
@keyframes zoomOutLeft {
40% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);
}
to {
opacity: 0;
transform: scale(0.1) translate3d(-2000px, 0, 0);
transform-origin: left center;
}
}
.zoomOutLeft {
animation-name: zoomOutLeft;
}
@keyframes zoomOutRight {
40% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);
}
to {
opacity: 0;
transform: scale(0.1) translate3d(2000px, 0, 0);
transform-origin: right center;
}
}
.zoomOutRight {
animation-name: zoomOutRight;
}
@keyframes zoomOutUp {
40% {
opacity: 1;
transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
to {
opacity: 0;
transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);
transform-origin: center bottom;
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
.zoomOutUp {
animation-name: zoomOutUp;
}
@keyframes slideInDown {
from {
transform: translate3d(0, -100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.slideInDown {
animation-name: slideInDown;
}
@keyframes slideInLeft {
from {
transform: translate3d(-100%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.slideInLeft {
animation-name: slideInLeft;
}
@keyframes slideInRight {
from {
transform: translate3d(100%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.slideInRight {
animation-name: slideInRight;
}
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.slideInUp {
animation-name: slideInUp;
}
@keyframes slideOutDown {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(0, 100%, 0);
}
}
.slideOutDown {
animation-name: slideOutDown;
}
@keyframes slideOutLeft {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(-100%, 0, 0);
}
}
.slideOutLeft {
animation-name: slideOutLeft;
}
@keyframes slideOutRight {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(100%, 0, 0);
}
}
.slideOutRight {
animation-name: slideOutRight;
}
@keyframes slideOutUp {
from {
transform: translate3d(0, 0, 0);
}
to {
visibility: hidden;
transform: translate3d(0, -100%, 0);
}
}
.slideOutUp {
animation-name: slideOutUp;
}
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none !important;
}
}
Проверьте себя
Выберите ответ — объяснение появится сразу. Ответ можно менять.
Задание
Ожидаемый результат: при открытии главной заголовок и три абзаца приветствия по очереди поднимаются и проявляются; строки не мелькают до начала своей анимации. После анимации приветствие выглядит как раньше.
Выполните задание
Условие задания
css/style.css
/* Общие стили сайта Тони */
/* Ширина и высота включают внутренние отступы и рамку — у всех элементов и псевдоэлементов */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Палитра сайта в пользовательских свойствах: цвет меняется в одном месте */
:root {
--accent: saddlebrown; /* заголовки, меню, акценты */
--text: #2b211b; /* основной текст */
--text-muted: #6b5d52; /* подписи и второстепенный текст */
}
/* Шрифт заголовков Nunito (лицензия SIL Open Font License): кириллица и латиница в разных файлах */
@font-face {
font-family: "Nunito";
src: url("../assets/nunito-cyrillic-800-normal.woff2") format("woff2");
font-weight: 800;
font-display: swap; /* пока шрифт загружается, текст виден запасным шрифтом */
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Nunito";
src: url("../assets/nunito-latin-800-normal.woff2") format("woff2");
font-weight: 800;
font-display: swap;
unicode-range: U+0000-00FF, U+2000-206F, U+20AC, U+2122;
}
body {
background-color: linen;
color: var(--text); /* цвет для потомков без собственного значения */
font-size: 1.0625rem; /* 17px при настройке браузера по умолчанию */
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif; /* системный шрифт устройства */
line-height: 1.6; /* множитель: у каждого элемента интервал от его собственного размера шрифта */
margin: 0; /* убираем отступ браузера: шапка и подвал во всю ширину окна */
}
/* Поля основного содержимого: текст не прижимается к краям окна */
main {
padding: 1.5rem 1rem;
max-width: 68rem; /* на широком экране строки не растягиваются на всё окно */
margin: 0 auto; /* колонка по центру */
}
/* Изображения и видео не шире колонки, пропорции сохраняются */
img,
video {
max-width: 100%;
height: auto;
}
/* Фреймы, аудиоплеер, индикаторы и поля формы не выходят за колонку */
progress,
meter,
iframe,
audio,
input,
select,
textarea,
button {
max-width: 100%;
}
/* Разрешаем группе полей сужаться вместе с колонкой. */
fieldset {
min-width: 0;
}
/* Общий цвет заголовков — групповой селектор */
h1,
h2 {
color: var(--accent);
font-family: "Nunito", "Trebuchet MS", system-ui, sans-serif;
font-weight: 800;
line-height: 1.2; /* крупному тексту хватает меньшего интервала */
}
/* Размеры заголовков в rem — от размера шрифта корня, а не родителя */
h1 {
text-align: center;
font-size: 2.5rem;
}
h2 {
font-size: 1.75rem;
}
/* Вводный блок главной страницы */
.intro {
background-color: hsl(30, 80%, 95%);
padding: 1.5rem; /* фон вокруг текста; заодно не выпускает margin заголовка за пределы блока */
border-radius: 16px; /* скругляется и фон, хотя рамки нет */
/* Лапка в правом нижнем углу; цвет фона выше остаётся запасным, если картинка не загрузится */
background-image: url("../assets/paw.svg");
background-repeat: no-repeat;
background-position: right 1rem bottom 1rem;
background-size: 4rem;
}
/* Разделы главной — светлые карточки на фоне страницы */
main > section {
background-color: #fffaf3;
border: 1px solid hsl(28, 30%, 82%);
border-radius: 16px;
padding: 1.5rem;
margin-top: 1.5rem;
}
/* Ширина вводных абзацев ограничена 60ch; количество букв в строке зависит от шрифта и текста */
.intro p {
max-width: 60ch;
}
/* Важное предупреждение во вводном блоке — темнее основного акцента */
.intro strong {
color: #6e3510;
}
/* Оглавление главной страницы — переносимый ряд «таблеток» на Flexbox */
.toc {
background-color: white;
border: 1px solid tan;
display: flex;
flex-wrap: wrap; /* «таблетки», которым не хватает места, переходят на новую строку */
gap: 0.5rem; /* промежутки между «таблетками» и между строками */
list-style: none;
padding: 0.75rem;
}
/* Подчёркивание ссылок чуть ниже букв: не задевает выносные элементы */
a {
text-underline-offset: 3px;
}
/* Тонкая линия отделяет шапку от страницы */
header {
border-bottom: 1px solid tan;
}
/* Меню на Flexbox: пункты могут переноситься, промежутки задаёт gap */
nav ul {
margin: 0;
padding: 0.5rem 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
}
/* Ссылка на запись стоит отдельно у правого края: автоматический отступ забирает свободное место */
nav li:last-child {
margin-left: auto;
}
/* Ссылки меню в шапке — селектор потомков */
nav a {
color: var(--accent);
text-decoration: none;
display: inline-block; /* у строчной ссылки вертикальный padding не раздвигает строку */
padding: 0.4rem 0.9rem;
border-radius: 999px; /* «таблетка»: радиус больше половины высоты */
}
/* Ссылки оглавления главной страницы */
.toc a {
color: var(--text-muted); /* тёплый серый: контраст 6.34:1 на белом фоне оглавления */
display: block; /* ссылка заполняет «таблетку» целиком */
padding: 0.25rem 0.75rem;
border: 1px solid tan;
border-radius: 999px;
text-decoration: none;
}
/* Фотографии со скруглёнными углами */
figure img {
border-radius: 8px;
}
figcaption {
color: var(--text-muted); /* тёплый серый: контраст 5.64:1 на фоне linen */
font-size: 0.95rem;
}
footer {
background-color: hsl(30, 50%, 90%);
text-align: center;
padding: 1rem;
}
/* Ссылка в подвале берёт цвет текста подвала */
footer a {
color: inherit;
}
/* Поля формы получают шрифт страницы вместо шрифта браузера */
input,
select,
textarea,
button {
font: inherit;
}
/* Ссылки темнеют при наведении указателя */
a:hover {
color: #5c2d0c;
}
/* Ссылки меню подчёркиваются при наведении и фокусе с клавиатуры */
nav a:hover,
nav a:focus-visible {
text-decoration: underline;
}
/* Заметная рамка фокуса у ссылок, полей и кнопок при работе с клавиатуры */
:focus-visible {
outline: 3px solid #1f6feb;
outline-offset: 3px;
}
/* Цитата из книги — шрифтом с засечками и курсивом */
blockquote {
font-family: Georgia, "Times New Roman", serif;
font-style: italic;
border-left: 4px solid tan; /* черта слева выделяет цитату */
padding-left: 1rem;
}
/* Подпись под цитатой — у правого края */
blockquote + figcaption {
text-align: right;
}
/* Строки формы на Flexbox: подпись — колонка постоянной ширины, поле занимает остаток строки,
а на узком экране переносится под подпись */
fieldset p {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
}
fieldset p label {
flex: 0 1 12rem; /* не растёт, но может сжаться в узкой строке */
min-width: 0;
}
fieldset p input,
fieldset p select,
fieldset p textarea {
flex: 1 1 12rem; /* растёт и сжимается от исходной ширины 12rem */
min-width: 0; /* поле может стать уже своего автоматического минимума */
}
/* Фотоальбом на Grid. Все правила альбома собраны во вложенный блок: & — сам .gallery */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
gap: 1rem;
list-style: none;
padding: 0;
/* Карточка — точка отсчёта для значка */
& li {
position: relative;
}
/* Карточка со значком выделена рамкой */
& li:has(.badge) {
outline: 3px solid tan;
outline-offset: 4px;
border-radius: 8px;
}
& figure {
margin: 0;
}
& img {
width: 100%; /* фото заполняет ширину своей ячейки */
}
}
/* Список команд: первая колонка подстраивается под текст, оставляя место для перевода. */
dl {
display: grid;
grid-template-columns: fit-content(calc(50% - 0.5rem)) minmax(0, 1fr);
gap: 0.5rem 1rem;
}
dt {
overflow-wrap: anywhere;
grid-column: 1;
font-weight: bold;
}
dd {
grid-column: 2;
overflow-wrap: anywhere; /* длинный перевод переносится внутри своей колонки */
margin: 0; /* браузерный отступ слева больше не нужен: колонку задаёт сетка */
}
/* Длинные слова и адреса переносятся, а не выходят за край узкого экрана */
body {
overflow-wrap: break-word;
}
/* Таблица режима дня на узком экране прокручивается в своей рамке, а не сжимается до нечитаемых колонок */
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
min-width: 28rem;
}
/* Подпись таблицы прокручивается вместе с ней: прижимаем её к началу, чтобы она была видна сразу */
caption {
text-align: left;
}
/* Широкий экран: в разделе «Моя любимая игрушка» фото справа, текст слева.
На узком экране правило не действует, и раздел остаётся одной колонкой. */
@media (width >= 48rem) {
.toy {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
column-gap: 1.5rem;
}
.toy figure {
grid-column: 2;
grid-row: 1 / span 6; /* фото рядом со всеми абзацами раздела */
margin: 0;
}
.toy hr {
width: 100%;
}
}
/* Значок «Любимая» в правом верхнем углу карточки; из потока исключён, поэтому фото не сдвигается */
.badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.2rem 0.7rem;
border-radius: 999px;
background-color: var(--accent);
color: white;
font-size: 0.85rem;
font-weight: bold;
}
/* Шапка прилипает к верхнему краю окна. Фон нужен, чтобы под шапкой не просвечивал текст страницы */
header {
position: sticky;
top: 0;
z-index: 10; /* выше карточек альбома: они тоже позиционированы и записаны в HTML позже шапки */
background-color: linen;
}
/* Липкая шапка должна быть низкой: пункты меню не сжимаются и не переносятся на вторую строку,
а если им не хватает места, меню прокручивается внутри шапки */
nav ul {
flex-wrap: nowrap;
overflow-x: auto;
}
nav li {
flex-shrink: 0;
}
/* Заголовок, к которому ведёт ссылка-якорь, не прячется под липкой шапкой.
В rem: при увеличении текста растут и шапка, и отступ */
[id] {
scroll-margin-top: 4.5rem;
}
/* Ссылка, которая открывается в новой вкладке, помечена стрелкой — посетитель знает, что страница не сменится */
a[target="_blank"]::after {
content: " ↗";
}
/* Ссылка на электронную почту — с конвертом */
a[href^="mailto:"]::before {
content: "✉ ";
}
/* Первая ссылка меню — «Главная» — подписана лапкой без изменения HTML */
nav li:first-child a::before {
content: "🐾 ";
}
/* Первый абзац вводного блока крупнее остальных. :first-child здесь не подойдёт — первым стоит заголовок h1 */
.intro p:first-of-type {
font-size: 1.2rem;
}
/* Список игрушек: вместо стандартного кружка — косточка */
.toy-list li::marker {
content: "🦴 ";
}
/* Номера шагов в нумерованных списках — в цвет заголовков и жирные; текст пунктов не меняется */
ol li::marker {
color: var(--accent);
font-weight: bold;
}
/* Таблица режима дня: соседние рамки объединяются в одну линию */
table {
border-collapse: collapse;
}
/* Ячейки с воздухом и линией снизу; текст — от левого края и сверху, даже если в соседней ячейке две строки */
th,
td {
padding: 0.6rem 0.8rem;
border-bottom: 1px solid tan;
text-align: left;
vertical-align: top;
}
/* Шапка таблицы — в цвет заголовков сайта */
thead th {
background-color: var(--accent);
color: white;
}
/* Чётные строки тела таблицы — на светлом фоне: глаз не теряет строку */
tbody tr:nth-child(even) {
background-color: hsl(30, 80%, 95%);
}
/* Отдельные рамки без промежутков позволяют скруглить крайние ячейки. Линия только снизу не удваивается. */
table {
border-collapse: separate;
border-spacing: 0;
}
thead th:first-child {
border-top-left-radius: 8px;
}
thead th:last-child {
border-top-right-radius: 8px;
}
/* Форма записи: текстовые поля, список и область текста в едином стиле */
input:not([type="radio"], [type="checkbox"]),
select,
textarea {
padding: 0.5rem 0.75rem;
border: 1px solid #8a7660;
border-radius: 8px;
background-color: white;
}
/* Поле «Ведёт прогулку» менять нельзя — фон показывает это */
input[readonly] {
background-color: hsl(30, 80%, 95%);
}
/* Отмеченные флажки и переключатели — в цвет сайта */
input[type="radio"],
input[type="checkbox"] {
accent-color: var(--accent);
}
/* Неверно заполненное поле — после того как посетитель с ним поработал */
input:user-invalid,
select:user-invalid,
textarea:user-invalid {
border-color: firebrick;
}
/* Группы полей — светлые блоки с тонкой рамкой вместо рельефной рамки браузера */
fieldset {
margin-bottom: 1rem;
border: 1px solid #8a7660;
border-radius: 16px;
}
/* Группа полей, в которой сейчас работает посетитель */
fieldset:focus-within {
border-color: var(--accent);
}
/* Кнопки-«таблетки»: основная залита цветом, второстепенная — только рамка */
button {
padding: 0.7rem 1.4rem;
border: 2px solid var(--accent);
border-radius: 999px;
background-color: var(--accent);
color: white;
font-weight: bold;
cursor: pointer;
}
button[type="reset"] {
background-color: transparent;
color: var(--accent);
}
/* Текст ошибок связан с полями через aria-describedby и не полагается только на цвет рамки. */
.field-error {
display: none;
flex-basis: 100%;
color: firebrick;
font-weight: bold;
}
input:user-invalid + .field-error {
display: block;
}
/* Фото любимой игрушки — как карточка-полароид: слегка повёрнуто вокруг нижнего края, будто прижато к столу */
.toy img {
transform: rotate(-2deg);
transform-origin: 50% 100%;
}
/* Под указателем фото выпрямляется и чуть увеличивается */
.toy img:hover {
transform: rotate(0deg) scale(1.03);
}
/* «Таблетка» оглавления под указателем и в фокусе приподнимается: отдельное свойство translate */
.toc a:hover,
.toc a:focus-visible {
translate: 0 -2px;
}
/* Нажатая кнопка будто вдавливается */
button:active {
transform: translateY(1px) scale(0.98);
}
/* Плавные изменения: переходим конкретные свойства, а не all */
.toy img {
transition: transform 0.4s ease-out; /* фото выпрямляется плавно */
}
.toc a {
transition: translate 0.2s ease-out;
}
button {
transition:
background-color 0.2s,
transform 0.1s;
}
/* Посетитель попросил систему уменьшить движение — изменения происходят сразу, без переходов */
@media (prefers-reduced-motion: reduce) {
.toy img,
.toc a,
button {
transition: none;
}
}
/* Лапка у «Главной» машет, когда на ссылку наводят указатель */
@keyframes wag {
0%,
100% {
rotate: 0deg;
}
25% {
rotate: -18deg;
}
75% {
rotate: 18deg;
}
}
nav li:first-child a::before {
display: inline-block; /* строчный псевдоэлемент не поворачивается */
transform-origin: 50% 80%; /* лапка качается от «запястья» */
}
nav li:first-child a:hover::before,
nav li:first-child a:focus-visible::before {
animation-name: wag;
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
animation-iteration-count: 2; /* помахать дважды и остановиться */
}
button:hover,
button:focus-visible {
background-color: #6b3513;
}
button[type="reset"]:hover,
button[type="reset"]:focus-visible {
background-color: hsl(30, 70%, 90%);
}
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Сайт карликового пинчера Тони: игрушки, режим дня и запись на прогулку.">
<title>Сайт Тони</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<!-- Меню сайта -->
<nav>
<ul>
<li><a href="index.html">Главная</a></li>
<li><a href="pages/toys.html">Мои игрушки</a></li>
<li><a href="pages/signup.html">Запись на прогулку</a></li>
</ul>
</nav>
</header>
<main>
<div class="intro">
<h1 id="top">Привет! Меня зовут Тони</h1>
<p>Я карликовый пинчер. Мне три года, я вешу 8 кг и люблю гулять в любую погоду. Хозяин говорит: <q>Тони — лучший сторож в доме</q>.</p>
<p>Мой день рождения — <time datetime="2023-09-22">22 сентября</time>. <strong>Пожалуйста, не кормите меня со стола!</strong></p>
<p>Моя родословная записана в Российской кинологической федерации (<abbr title="Российская кинологическая федерация">РКФ</abbr>).</p>
</div>
<ul class="toc">
<li><a href="#toy">Моя любимая игрушка</a></li>
<li><a href="#commands">Команды, которые я знаю</a></li>
<li><a href="#schedule">Мой режим дня</a></li>
<li><a href="#walks">Где я гуляю</a></li>
<li><a href="#song">Песня для прогулок</a></li>
</ul>
<section class="toy">
<h2 id="toy">Моя любимая игрушка</h2>
<p data-toy-id="17">Это канат с мячиком. Хозяин бросает его, а я приношу обратно — <em>почти</em> всегда. <a href="pages/toys.html">Посмотреть все игрушки Тони</a>.</p>
<figure>
<img src="assets/tony-toy.png" alt="Тони сидит и держит в зубах канат с красным мячиком" width="300" height="400" title="Мой канат "Удав"">
<figcaption>Я и мой канат «Удав»</figcaption>
</figure>
<p><a href="assets/tony-toy.png" download>Скачать фотографию</a></p>
<p>Подробнее о моей породе — в статье <a href="https://ru.wikipedia.org/wiki/Карликовый_пинчер" target="_blank">«Карликовый пинчер» в Википедии</a>.</p>
<hr>
<p>Вечером игра закончилась. Канат остался на полу, а я свернулся калачиком в кресле.</p>
</section>
<section>
<h2>Мой стишок</h2>
<p>
Я маленький, но смелый,<br>
Гуляю целый день,<br>
А вечером на кресле<br>
Сплю — и мне не лень.
</p>
</section>
<section>
<h2 id="commands">Команды, которые я знаю</h2>
<p>Хозяин научил меня командам на немецком:</p>
<dl>
<dt lang="de">Sitz!</dt>
<dd>сидеть</dd>
<dt lang="de">Platz!</dt>
<dd>лежать</dd>
<dt lang="de">Fuß!</dt>
<dd>рядом</dd>
</dl>
<details>
<summary>Почему команды на немецком?</summary>
<p>Карликовый пинчер — немецкая порода, и хозяину понравилась эта традиция.</p>
</details>
</section>
<section>
<h2 id="schedule">Мой режим дня</h2>
<div class="table-wrap" tabindex="0" role="region" aria-label="Режим дня Тони">
<table>
<caption>Режим дня Тони</caption>
<thead>
<tr>
<th scope="col">Время</th>
<th scope="col">Будни</th>
<th scope="col">Выходные</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">07:00</th>
<td>Прогулка, 20 минут</td>
<td>Сплю</td>
</tr>
<tr>
<th scope="row">08:00</th>
<td colspan="2">Завтрак</td>
</tr>
<tr>
<th scope="row">13:00</th>
<td>Жду хозяина и сплю</td>
<td>Прогулка в парке, 1 час</td>
</tr>
<tr>
<th scope="row">19:00</th>
<td colspan="2">Ужин</td>
</tr>
<tr>
<th scope="row">21:00</th>
<td colspan="2">Вечерняя прогулка</td>
</tr>
</tbody>
</table>
</div>
</section>
<section>
<h2 id="progress">Мои успехи</h2>
<p>Учу команду «Апорт»: пройдено 3 занятия из 5. <progress value="3" max="5">3 из 5</progress></p>
<p>Прогулка сегодня: 40 минут. <meter value="40" min="0" max="90" low="30" high="60" optimum="75">40 из 90 минут</meter></p>
</section>
<section>
<h2 id="walks">Где я гуляю</h2>
<p>Я люблю гулять в сквере у дома.</p>
<p>
<a href="pages/walks.html" target="walk">Маршрут</a> |
<a href="assets/tony.png" target="walk">Фото с прогулки</a>
</p>
<iframe src="pages/walks.html" name="walk" title="Маршрут и фото прогулки Тони" width="320" height="420"></iframe>
<h3>Видео с прогулки</h3>
<p>Субтитры помогут, если вы смотрите видео без звука.</p>
<video width="180" height="320" poster="assets/walk.jpg" controls>
<source src="assets/walk.mp4" type="video/mp4">
<track src="assets/ru.vtt" kind="subtitles" srclang="ru" label="Русский" default>
<p><a href="assets/walk.mp4">Скачать видео</a></p>
</video>
<h3>Как я лаю</h3>
<p>Так я разговариваю с хозяином — у нас целый диалог!</p>
<audio controls>
<source src="assets/bark.mp3" type="audio/mpeg">
<p><a href="assets/bark.mp3">Скачать запись</a></p>
</audio>
</section>
<section>
<h2 id="song">Песня для прогулок</h2>
<p>Хозяин включает эту песню, когда мы выходим гулять.</p>
<iframe width="480" height="270" src="https://www.youtube.com/embed/ojULkWEUsPs?start=17" title="Baha Men — Who Let the Dogs Out" loading="lazy" allow="fullscreen"></iframe>
<p><a href="https://www.youtube.com/watch?v=ojULkWEUsPs">Baha Men — Who Let the Dogs Out на YouTube</a></p>
</section>
<section>
<h2>Моя любимая книга</h2>
<p>Хозяин читал мне вслух рассказ <cite>«Каштанка»</cite> Антона Чехова. Он начинается так:</p>
<figure>
<blockquote>
<p>Молодая рыжая собака — помесь таксы с дворняжкой — очень похожая мордой на лисицу, бегала взад и вперёд по тротуару и беспокойно оглядывалась по сторонам.</p>
</blockquote>
<figcaption>— Антон Чехов, <cite>«Каштанка»</cite></figcaption>
</figure>
</section>
<p><a href="#top">Наверх</a></p>
</main>
<footer>
<p><small>© 2026 Тони и его хозяин</small></p>
<address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
</footer>
</body>
</html>
pages/signup.html
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Запишитесь на совместную прогулку с карликовым пинчером Тони.">
<title>Запись на прогулку с Тони</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<header>
<nav>
<ul>
<li><a href="../index.html">Главная</a></li>
<li><a href="toys.html">Мои игрушки</a></li>
<li><a href="signup.html">Запись на прогулку</a></li>
</ul>
</nav>
</header>
<main>
<h1>Запись на прогулку</h1>
<p>Гуляйте вместе с Тони! Оставьте заявку, и хозяин Тони напишет вам, чтобы договориться о встрече.</p>
<p><a href="../index.html#walks">Где мы гуляем</a></p>
<form>
<fieldset>
<legend>О вас</legend>
<p>
<label for="owner">Ваше имя</label>
<input type="text" id="owner" name="owner" autocomplete="name" required aria-describedby="owner-error">
<span class="field-error" id="owner-error">Введите ваше имя.</span>
</p>
<p>
<label for="email">Электронная почта</label>
<input type="email" id="email" name="email" autocomplete="email" placeholder="anna@example.com" required aria-describedby="email-error">
<span class="field-error" id="email-error">Введите адрес электронной почты, например anna@example.com.</span>
</p>
</fieldset>
<fieldset>
<legend>Ваша собака</legend>
<p>
<label for="breed">Порода</label>
<input type="text" id="breed" name="breed" list="breeds">
<datalist id="breeds">
<option value="Карликовый пинчер"></option>
<option value="Такса"></option>
<option value="Йоркширский терьер"></option>
<option value="Беспородная"></option>
</datalist>
</p>
<p>
<label for="dogs">Сколько собак придёт</label>
<input type="number" id="dogs" name="dogs" min="1" max="3" value="1" aria-describedby="dogs-error">
<span class="field-error" id="dogs-error">Укажите число собак от 1 до 3.</span>
</p>
</fieldset>
<fieldset>
<legend>Когда гулять</legend>
<p>
<label for="date">Дата</label>
<input type="date" id="date" name="date" required aria-describedby="date-error">
<span class="field-error" id="date-error">Выберите дату прогулки.</span>
</p>
<label><input type="radio" name="time" value="morning" checked> Утром</label>
<label><input type="radio" name="time" value="evening"> Вечером</label>
</fieldset>
<fieldset>
<legend>Прогулка</legend>
<p>
<label for="leader">Ведёт прогулку</label>
<input type="text" id="leader" name="leader" value="Тони" readonly>
</p>
<p>
<label for="route">Маршрут</label>
<select id="route" name="route">
<optgroup label="В городе">
<option value="square">Сквер у дома</option>
<option value="pond" selected>Пруд с утками</option>
</optgroup>
<optgroup label="За городом">
<option value="forest">Лес</option>
<option value="river" disabled>Берег реки — закрыт до весны</option>
</optgroup>
</select>
</p>
<p>
<label for="wishes">Пожелания</label>
<textarea id="wishes" name="wishes" rows="4" cols="40" maxlength="300" placeholder="Например: моя собака боится велосипедов"></textarea>
</p>
</fieldset>
<fieldset>
<legend>Что взять с собой</legend>
<label><input type="checkbox" name="take" value="ball"> Мячик</label>
<label><input type="checkbox" name="take" value="rope"> Канат</label>
</fieldset>
<p>
<button type="submit">Записаться на прогулку</button>
<button type="reset">Очистить форму</button>
</p>
</form>
</main>
<footer>
<p><small>© 2026 Тони и его хозяин</small></p>
<address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
</footer>
</body>
</html>
pages/toys.html
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Любимые игрушки Тони и правила игры в канат.">
<title>Игрушки Тони</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<header>
<nav>
<ul>
<li><a href="../index.html">Главная</a></li>
<li><a href="toys.html">Мои игрушки</a></li>
<li><a href="signup.html">Запись на прогулку</a></li>
</ul>
</nav>
</header>
<main>
<h1>Мои игрушки</h1>
<ul class="toy-list">
<li>Канат с мячиком «Удав» — моя любимая игрушка</li>
<li>Резиновая косточка — для вечеров, когда гулять уже поздно</li>
<li>Пищащий ёжик — его я прячу <del>под диваном</del> <ins>в своей лежанке</ins></li>
</ul>
<img src="../assets/tony.png" alt="Тони сидит и смотрит в камеру" width="300" height="400">
<h2>Как играть со мной в канат</h2>
<ol>
<li>Покажите мне канат.</li>
<li>Бросьте его подальше.</li>
<li>Подождите, пока я принесу его обратно.</li>
<li>Похвалите меня — и всё сначала!</li>
</ol>
<section>
<h2 id="album">Мой фотоальбом</h2>
<ul class="gallery">
<li>
<figure>
<img src="../assets/album-puppy.png" alt="Тони-щенок сидит и смотрит в камеру" width="300" height="400">
<figcaption>Я в детстве</figcaption>
</figure>
</li>
<li>
<figure>
<img src="../assets/album-squirrel.png" alt="Тони лежит в обнимку с плюшевой белкой" width="300" height="400">
<figcaption>С плюшевой белкой</figcaption>
</figure>
<span class="badge">Любимая</span>
</li>
<li>
<figure>
<img src="../assets/album-hoodie.png" alt="Тони стоит в бежевой толстовке с капюшоном" width="300" height="400">
<figcaption>В худи для холодной погоды</figcaption>
</figure>
</li>
<li>
<figure>
<img src="../assets/album-harness.png" alt="Тони в оранжевой шлейке ждёт прогулку" width="300" height="400">
<figcaption>Шлейка надета — идём гулять!</figcaption>
</figure>
</li>
<li>
<figure>
<img src="../assets/album-laugh.png" alt="Тони лежит, запрокинув голову, и широко открыл пасть, будто смеётся" width="300" height="400">
<figcaption>Смеюсь над шуткой хозяина</figcaption>
</figure>
</li>
<li>
<figure>
<img src="../assets/album-sleep.png" alt="Тони спит, положив голову на лапы" width="300" height="319">
<figcaption>После прогулки</figcaption>
</figure>
</li>
</ul>
</section>
<p><a href="../index.html#commands">Какие команды я знаю</a></p>
<p><a href="../index.html">Вернуться на главную</a></p>
</main>
<footer>
<p><small>© 2026 Тони и его хозяин</small></p>
<address>Написать хозяину Тони: <a href="mailto:tony@example.com">tony@example.com</a></address>
</footer>
</body>
</html>
pages/walks.html
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Маршрут прогулки Тони: сквер, пруд и поляна для игры с канатом.">
<title>Где гуляет Тони</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<h1>Мой маршрут</h1>
<ol>
<li>Сквер у дома.</li>
<li>Пруд с утками.</li>
<li>Поляна для игры с канатом.</li>
</ol>
</body>
</html>
Продолжите в VS Code
Внесите те же правила в css/style.css своего сайта my-site и обновите главную страницу.
Проверка: в Chrome откройте инструменты разработчика, меню ⋮ → More tools → Rendering, в
пункте Emulate CSS media feature prefers-reduced-motion выберите reduce и обновите страницу:
приветствие видно сразу, без движения, а лапка в меню не машет. Если результат отличается, нажмите в редакторе
«Показать решение» и сравните его со своим кодом.