Пишем таймер обратного отсчёта на чистом CSS
CSS – мощный инструмент современного разработчика. Он многое умеет, в нём есть переменные, функции, наследование и ещё много крутых штук. Но всё-таки это не язык программирования – у него совсем другая сфера ответственности. Тем интереснее использовать CSS для решения задач программирования 🙂 Именно этим сегодня и займёмся.
Дисклеймер! Многие вещи в принципе невозможно сделать на CSS. Ещё больше вещей делать на CSS нерационально. Мы занимаемся этим только из болезненного любопытства и стремления познать все скрытые возможности инструмента.
Условие задачи
Нужно сделать таймер обратного отсчёта. Предъявляемые требования:
- Таймер должен выводить миллисекунды от 99 до 0.
- Когда остаётся меньше 10 миллисекунд, нужно выводить только одну цифру (от 9 до 0) и центрировать её.
- Бонус #1: Цвет шрифта и фона можно настраивать в процессе работы (без кусочка JS не обойтись).
- Бонус #2. После остановки таймера его можно перезапустить.
- Код должен работать и на ПК, и на мобильных устройствах.
- Чтобы выполнить указанные условия, пойдём напролом. Все нужные цифры (от 0 до 9) запишем прямо в базовую разметку страницы. Затем для имитации таймера анимируем их в нужном ритме и правильной последовательности.Да, не очень элегантно. Но сработает, вот увидите.Что нам потребуется?
- CSS трансформации
- CSS анимации
- Flexbox-модель
- CSS переменные
- Различные селекторы
Вот что получится в итоге:
See the Pen
Pure CSS countdown from 99 to 0 by Chen Hui Jing (@huijing)
on CodePen.
Реализация на JavaScript
Сразу посмотрим, как это можно было сделать на JS.
Простой и понятный код, состоящий из пары функций. Для обновления таймера подписываемся на момент перерисовки браузера с помощью метода window.requestAnimationFrame().
let end; const now = Date.now; const timer = document.getElementById("timer"); const duration = 9900; function displayCountdown() { const count = parseInt((end - now()) / 100); timer.textContent = count > 0 ? (window.requestAnimationFrame(displayCountdown), count) : 0; } function start() { end = now() + duration; window.requestAnimationFrame(displayCountdown); }
Всего пара блоков в HTML:
<div class="timer-container"> <p class="timer" id="timer">99</p> </div>
И элементарные стили для выравнивания:
.timer-container { display: flex; height: 100vh; } .timer { margin: auto; }
Согласитесь, ровным счётом ничего интересного. Поэтому бросаем эту ерунду и идём писать по-настоящему крутой таймер.
Общий подход
Сложно придумать более прямое решение, чем простое перечисление в HTML всех цифр. Расположим их в две группы (два разряда). По мере необходимости будем скрывать ненужные символы.
<div class="timer"> <div class="digit seconds"> <span>9</span> <span>8</span> <span>7</span> <span>6</span> <span>5</span> <span>4</span> <span>3</span> <span>2</span> <span>1</span> <span>0</span> </div><div class="digit milliseconds"> <span>9</span> <span>8</span> <span>7</span> <span>6</span> <span>5</span> <span>4</span> <span>3</span> <span>2</span> <span>1</span> <span>0</span> </div> </div>
Анимация будет заключаться в простом прокручивании каждого блока по вертикали. В каждый момент времени в каждой колонке будет отображаться только одна цифра.
CSS трансформации
Большинство CSS-свойств плохо подходят для анимирования, так как их изменение вызывает перерисовку страницы. Но есть два «безопасных» свойства, которыми мы можем воспользоваться: transform
и opacity
.
Подробнее об анимации в вебе можно прочитать в замечательном руководстве High Performance Animations.
Для оживления таймера возьмём надёжное свойство translateY
. Оно обеспечит перемещение блока только по y
-оси.
.selector { transform: translateY(0); }
Можно воспользоваться и полным свойством translate
, но помните, что его первый аргумент соответствует x
-координате. Если хотите перемещать элемент только по вертикали, то передайте первым параметром 0
.
.selector { transform: translate(3em); } /* то же самое */ .selector { transform: translate(3em, 0); }
Чтобы лучше понять функции трансформации, загляните в спецификацию CSS Transforms Module Level 1. Там всё разобрано на подробных примерах, так что вы разберётесь, даже если не очень любите математику.
CSS animations
Следующий шаг – анимировать применение трансформаций. Для этого мы используем CSS-анимации.
Самое важное правило, которое вы должны знать, – это @keyframes. Оно позволяет разбить анимацию на кадры и описать каждый из них в отдельности.
@keyframes seconds { 0% { transform: translateY(0) } 10% { transform: translateY(-1em) } 20% { transform: translateY(-2em) } 30% { transform: translateY(-3em) } 40% { transform: translateY(-4em) } 50% { transform: translateY(-5em) } 60% { transform: translateY(-6em) } 70% { transform: translateY(-7em) } 80% { transform: translateY(-8em) } 90% { transform: translateY(-10em); width: 0; } 100% { transform: translateY(-10em); width: 0; } } @keyframes milliseconds { 0% {transform: translateY(0) } 10% { transform: translateY(-1em) } 20% { transform: translateY(-2em) } 30% { transform: translateY(-3em) } 40% { transform: translateY(-4em) } 50% { transform: translateY(-5em) } 60% { transform: translateY(-6em) } 70% { transform: translateY(-7em) } 80% { transform: translateY(-8em) } 90% { transform: translateY(-9em) } 100% { transform: translateY(-9em) } }
Здесь мы создали две анимации – по одной для каждого блока с цифрами.
Обратите внимание на два последних кадра в анимации первого блока. В этот момент там должна отображаться цифра 0
, но она нам не нужна, поэтому скрываем её с помощью width: 0
.
Чтобы применить анимации, используем краткий синтаксис свойства animation
:
.seconds { animation: seconds 10s 1 step-end forwards; } .milliseconds { animation: milliseconds 1s 10 step-end forwards; }
Если вы зайдете в панель инструментов разработчика и откроете вкладку с вычисленными значениями (computed), то увидите, что вместо одного свойства animation
к элементу применились сразу несколько:
animation-name
Имя анимации, использующееся для её идентификации. Для него можно использовать латинские буквы, цифры, нижнее подчёркивание и дефисы. Первой должна идти буква. В начале не могут стоять зарезервированные слова none
, unset
, initial
или inherit
, а также сочетание --
. Регистр символов имеет значение.
animation-duration
Продолжительность одного цикла анимации. Для первой колонки цифр анимация будет длиться 10 секунд (10s
). Вторая колонка двигается в 10 раз быстрее (1s
).
animation-iteration-count
Количество циклов анимации, которое должно выполниться до ее остановки. Первую колонку нужно прокрутить лишь один раз – от 9 до 0. Вторую – целых 10 раз, по одному на каждое положение первой колонки.
animation-timing-function
Это свойство описывает прогресс анимации в течение одного цикла. Если вы знакомы с кривыми Безье, то можете контролировать его до мельчайших подробностей с помощью функции cubic-bezier()
. Для простых смертных есть несколько готовых значений animation-timing-function, обозначенных ключевыми словами (ease
, ease-in
и т.д)
Нам же больше подойдёт значение step-end
. Это то же самое, что и steps(1, jump-end)
.
Функция steps()
разбивает анимацию на равные «шаги», то есть величина изменяется не плавно, а прерывисто. Первый аргумент – количество шагов, второй – момент, когда начинается анимация. Ключевое слово jump-end
означает, что анимация запускается в конце, а не начале каждого шага.
Чтобы лучше разобраться в этой функции, обратитесь к статье Дэна Уилсона Jumps: The New Steps() in Web Animation.
animation-fill-mode
Состояние целевого объекта до и после завершения анимации. Нам требуется, чтобы колонки останавливались на последней цифре (последний ключевой кадр), поэтому используем значение forwards
.
Когда анимация остановится, первая цифра будет скрыта, а вторая колонка замрёт на позиции -9em
.
Ещё больше о CSS анимациях вы можете узнать в спецификации CSS Animations Level 1.
Flexbox
Цифру 0 в первом разряде мы уже скрыли с помощью инструкции @keyframes
, осталось только выровнять таймер по центру страницы:
.timer-container { display: flex; height: 100vh; } .timer { overflow: hidden; margin: auto; height: 1em; width: 2ch; text-align: center; } .digit { display: inline-block; } .digit span { display: block; width: 100%; height: 1em; }
Для вертикального выравнивания используем автоматический расчёт маргинов у потомка флекс-контейнера. Другими словами, назначаем display: flex
родительскому блоку, и margin: auto
дочернему.
Горизонтальное выравнивание достигается обычным text-align: center
.
Больше информации о Flex-модели – в спецификации CSS Flexible Box Layout Module Level 1.
Бонус #1: Динамическое изменение цвета
В условиях задачи была также бонусная возможность кастомизации цвета текста и фона нашего таймера.
Используем для ее решения кастомные свойства CSS и инпут выбора цвета.
Положим цвета в переменные:
:root { --fontColour: #000000; --bgColour: #ffffff; }
И используем их в таймере:
.timer { background-color: var(--bgColour, white); } .digit { color: var(--fontColour, black); }
Добавим на страницу два инпута для изменения цветовой схемы:
<aside> <label> <span>Font colour:</span> <input id="fontColour" type="color" value="#000000" /> </label> <label> <span>Background colour:</span> <input id="bgColour" type="color" value="#ffffff" /> </label> </aside>
Теперь придётся написать пару строк JS-кода, чтобы динамически изменять переменные цветов при изменении инпутов:
let root = document.documentElement; const fontColourInput = document.getElementById('fontColour'); const bgColorInput = document.getElementById('bgColour'); fontColourInput.addEventListener('input', updateFontColour, false); bgColorInput.addEventListener('input', updateBgColour, false); function updateFontColour(event) { root.style.setProperty('--fontColour', event.target.value); } function updateBgColour(event) { root.style.setProperty('--bgColour', event.target.value); }
Да, не всё можно решить на чистом CSS.
Бонус #2: Перезапуск таймера
Сейчас таймер отрабатывает только один раз. Чтобы запустить его сначала, необходимо перезагрузить страницу. Давайте добавим возможность перезапуска.
Возможно, вы подумали, что нам придётся снова смошенничать и добавить JavaScript? А вот и нет, мы справимся своими силами!
Воспользуемся чекбоксом, который обладает состоянием checked
. Мы легко можем получить к нему доступ из CSS.
.toggle span { font-size: 1.2em; padding: 0.5em; background-color: palegreen; cursor: pointer; border-radius: 4px; } input[type="checkbox"] { opacity: 0; position: absolute; } input[type="checkbox"]:checked ~ aside .toggle span:first-of-type { display: none; } .toggle span:nth-of-type(2) { display: none; } input[type="checkbox"]:checked ~ aside .toggle span:nth-of-type(2) { display: inline; }
Теперь анимация работает только при включенном чекбоксе, а при выключенном таймер сбрасывается и возвращается в исходное состояние.
input[type="checkbox"]:checked ~ .timer .seconds { animation: seconds 10s 1 step-end forwards; } input[type="checkbox"]:checked ~ .timer .milliseconds { animation: milliseconds 1s 10 step-end forwards; }
Важно, чтобы чекбокс находился на одном уровне с таймером и стоял в разметке перед ним. Это даст нам возможность воспользоваться селектором «сиблингов» (~
). А лейбл для него может находиться где угодно.