
Хабр, привет!
На носу Новый год. Все друг друга поздравляют. Мне это время очень нравится. Для меня Новый год — самый главный праздник.
В общем, мы же фронтендеры. Я подумал: а почему бы не придумать специальную открытку с поздравлением? Использовать я буду только HTML и CSS. Никаких картинок. Да-да, даже векторного SVG.
Единственным «хаком» будет шрифт — мы подберём красивый. Также у нас будет анимация снега. В общем, сделаем всё по красоте!
В ходе реализации я буду использовать все последние фишки CSS. Вдруг вы о них не слышали — заодно и что-то полезное для себя найдёте.
Давайте уже сделаем новогоднюю открытку!
Подготовка
Мы не будем использовать какие-то готовые решения для создания открытки. По этой причине нам нужно сделать несколько подготовительных действий.
Начнём мы с подключения шрифта. Для этой статьи я буду использовать сайт Codepen. По этой причине мне легче подключить шрифт с помощью функции @import. Пожалуйста, потом исправьте стандартный метод подключения с помощью элемента link.
Так я полазил по сайту Google Fonts, и мне понравился шрифт Mark Script. Его мы и будем использовать.
@import url('https://fonts.googleapis.com/css2?family=Marck+Script&display=swap');Далее нам нужно сделать несколько манипуляций для элемента body. Давайте сбросим значение для свойства margin, установим цвет страницы и минимальную высоту на весь вьюпорт.
Для последней задачи мы будем использовать единицы измерения dvh. Они более надёжны при отображении на смартфонах и планшетах.
body {
background-color: #b4dce4;
margin: 0;
min-height: 100dvh;
}Далее установим основные стили текста страницы. Ранее мы уже подключили шрифт Marck Script. Он будет основным на странице. В качестве безопасной пары к нему я буду использовать ключевое слово system-ui.
В этом случае, если у нас основной шрифт не загрузится, то браузеры будет использовать шрифт операционной системы устройства, на котором загружена страница. И он будет тоже красивым!
Размер текста объявим 2rem, а цвет — #668f9a.
body {
background-color: #b4dce4;
margin: 0;
min-height: 100dvh;
font-family: "Marck Script", system-ui;
font-size: 2rem;
color: #668f9a;
}Теперь надо сбросить браузерные стили у элементов. В основном в разметке у нас будут элементы div. Но для текста поздравления мне не позволяет совесть его использовать. Тут нужен элемент p.
У него надо сбросить значение для свойства margin. Пожалуйста, не надо делать так.
p {
margin-top: 0;
margin-bottom: 0;
}Во-первых, не надо сбрасывать стили только через селектор по типу. Есть много ситуаций, когда это выйдет вам боком. Например, если будете работать на проектах, где редакторы заполняют контент. При таком селекторе вёрстка точно поедет, потеряв нужные отступы.
Во-вторых, значение для свойства margin можно сбросить за одну строку. Свойство margin-block устанавливает значение сразу с двух сторон. По умолчанию для русского языка, как раз по вертикали.
Учитывая эти нюансы, я буду использовать селектор по типу вместе с селектором по атрибуту. Такое решение мне нравится тем, что атрибут class я всегда добавляю в интерфейсную часть приложения, где и нужно сбросить стили.
Плюс к интерфейсной части есть доступ только у разработчиков, следовательно, контент-менеджеры ничего не сломают.
p[class] {
margin-block: 0;
}Текст поздравления
Отлично, мы дошли до этапа вёрстки текста поздравления. Для него я уже подготовил разметку.
<body>
<div class="congrats">
<div class="congrats__attention">
<span class="congrats__name">Екатерина,</span>
<span class="congrats__new-year">c Новым годом!</span>
</div>
<div class="congrats__wish">
<p class="congrats__message">Я желаю тебе удачи и успехов. Пусть новый год будет таким, каким ты хочешь!</p>
<p class="congrats__author">Константин Мельников, Декабрь 2025</p>
</div>
</div>
</body>
Сейчас текст нашей открытки отображается не очень.
Первая наша задача будет отобразить имя получателя и надпись «с Новым годом» в две строки. Но тут я хочу сделать хитрость. Мне нужно сделать так, чтобы надпись была строго за именем получателя. Также учтём, что оно может занимать разное пространство.
Для решения задачи я буду использовать гриды. Первая колонка должна будет подстраиваться под длину имени. Это я реализую с помощью ключевого слова min-content.
.congrats__attention {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-rows: repeat(2, 1fr);
}
.congrats__new-year {
grid-column: 2;
grid-row: 2;
}
Далее наведём обычную красоту. Расставим все размеры, отступы и всё такое.
.congrats__attention {
font-size: 4.5rem;
line-height: 1;
}
.congrats__wish {
margin-top: 1.75rem;
display: grid;
gap: 3rem;
}
.congrats__message {
text-align: center;
}
.congrats__author {
display: flex;
align-items: center;
font-size: .75em;
justify-self: end;
}
.congrats__author::before {
content: "";
width: 25px;
height: 2px;
margin-right: .5em;
background-color: currentColor;
}
А теперь перейдём к позиционированию текста. Сделаем его более аккуратно по центру.
Для этого поработаем с элементом body. Вся наша открытка будет разделена на две области: основную, где отобразится текст, и нижнюю (там будет снег и сугробы).
Для этого я буду использовать гриды. Важный нюанс заключается в том, что под область со снегом мне нужно зарезервировать место с учётом будущей анимации.
Сугробы будут расти в высоту. По этой причине я буду использовать максимальное значение в анимации 20dvh.
body {
display: grid;
grid-template-rows: 1fr 20dvh;
}
Поскольку мы объявили свойство display со значением grid, то мы можем использовать свойство margin со значением auto, чтобы расположить элементы по центру по двум осям сразу.
.congrats {
margin: auto;
}
Осталась последняя задача. Сейчас у нас типографика выглядит не очень равномерной. Это лучше будет заметно на коротком имени. Например, попробуем имя Ян, добавив его в инструментах разработчика.

Что я хочу сделать. Мы весь текст отцентрируем. Только это не так просто, как кажется.
Вся загвоздка в элементе с классом .congrats__attention. В нём может быть абсолютно любое имя. Чтобы отцентрировать такой элемент, ему нужно явно указать значение для свойства width. Но какое?
Здесь я буду использовать ключевое слово fit-content. Браузеры с помощью него рассчитают значение для свойства width строго по тексту.
.congrats__attention {
width: fit-content;
margin-inline: auto;
}Остался элемент с поздравлением. Его я сделаю немного уже и тоже отцентрирую.
.congrats__message {
max-width: 75%;
margin-inline: auto;
}
Для тестирования вёрстки я использовал имена «Ян», «Полина», «Станислав» и «Александреевич».




Снег
Снег у нас будет реализован несколькими способами. Первый — это нижняя часть открытки, которая будет имитировать выпавший снег.
Технически этот элемент будет реализован прямоугольником.
<body>
<!-- тут разметка текста поздравления -->
<div class="snowbank"></div>
</body>.snowbank {
width: 100%;
height: 10dvh;
background-color: #fdfdff;
position: relative;
margin-top: auto;
}
Также мы сделаем два сугроба. Интересным техническим нюансом будет использование свойства aspect-ratio.
Если вы захотите изменить размеры сугробов, то оно поможет сохранить изначальную пропорцию.
.snowbank::before,
.snowbank::after {
content: "";
height: 15vh;
aspect-ratio: 2;
border-radius: 50%;
background-color: inherit;
position: absolute;
top: -6dvh;
}
.snowbank::before{
left: 10%;
}
.snowbank::after{
right: 10%;
}
Остались снежинки. У нас будет много элементов div. Каждый будет отвечать за пару снежинок. Всего потребуется девятнадцать штук. Я покажу разметку частично, полную версию вы можете посмотреть в конце статьи.
<body>
<!-- тут разметка текста поздравления -->
<div class="snowbank"></div>
<div class="snow snow_1"></div>
<!-- ещё 17 дивов -->
<div class="snow snow_19"></div>
</body>Каждая снежинка создаётся с помощью кружочка. По умолчанию мы их скроем с помощью свойства opacity, чтобы был эффект появления. Также нам нужно расположить их в верху открытки, чтобы снег шёл сверху вниз.
.snow::before,
.snow::after {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #fefefe;
opacity: 0;
position: absolute;
top: 0;
}Далее нужно распределить все снежинки по открытке. Как и с разметкой, не буду показывать все классы. Вы их сможете посмотреть в конце статьи.
.snow_1::before {
left: 10%;
translate: 0 1dvh;
}
.snow_1::after {
left: 20%;
translate: 0 1.5dvh;
}
/* другие классы снежинок */
.snow_19::before {
left: 83%;
translate: 0 5.5dvh;
}
.snow_19::after {
left: 89%;
translate: 0 5.5dvh;
}Анимация
Теперь мы можем всё это дело сделать живым. Мы создадим три анимации. Для области со снегом, для сугробов и снежинок. Начнём с первой.
Область со снегом мы будем плавно увеличивать по высоте. Как будто снега становится всё больше и больше. Реализован данный эффект будет с помощью свойства height, у которого будет изменено значение от 10dvh до 20dvh.
.snowbank {
/* оставшиеся стили */
animation: snowbank 30s linear forwards;
}
@keyframes snowbank {
0%, 28%{
height: 10dvh;
}
30%, 63%{
height: 13dvh;
}
65%, 100%{
height: 20dvh;
}
}Сугробы у нас тоже будут расти в высоту.
.snowbank::before,
.snowbank::after {
/* оставшиеся стили */
animation: snowdrifts 30s linear forwards;
}
@keyframes snowdrifts {
0%, 28%{
height: 15dvh;
top: -6dvh;
}
30%, 63%{
height: 16dvh;
top: -7dvh;
}
65%, 100%{
height: 17dvh;
top: -8dvh;
}
}Как я говорил выше, снег будет идти сверху вниз, плавно появлялась. Изменение позиции снежинки будет происходить с помощью свойства translate, а за появление будет отвечать пара свойств opacity и scale.
Также, чтобы снежинки не летели параллельно друг с другом, нужно расставить задержки в анимации. Так они будут лететь по-разному.
.snow::before,
.snow::after {
/* оставшиеся стили */
animation-name: snow;
animation-duration: 8000ms;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.snow_1::before {
left: 10%;
translate: 0 1dvh;
}
.snow_1::after {
left: 20%;
translate: 0 1.5dvh;
animation-delay: 200ms;
}
/* другие классы снежинок */
.snow_19::before {
left: 83%;
translate: 0 5.5dvh;
animation-delay: 6666ms;
}
.snow_19::after {
left: 89%;
translate: 0 5.5dvh;
animation-delay: 6200ms;
}
@keyframes snow {
0% {
translate: 0 0;
}
10% {
opacity: 1;
}
100% {
translate: 0 105dvh;
scale: 1.2;
}
}
В анимации я использую значение 105dvh. В результате снежинки будут уходить за пределы вьюпорта, создавая полосу прокрутки. Для исправления этого эффекта добавим overflow со значением hidden к элементу body .
body {
overflow: hidden;
}Вместо заключения
Я хочу поздравить вас с наступающим Новым годом. Спасибо, что читаете мои статьи. Вы приносите хлеб в мой дом. Даже хейтеры. Вы — важная часть моей работы. Думаю, именно вы как раз и разгоняете мои статьи :)
Я хочу вам пожелать в новом году побольше новых открытий для себя. Пусть он будет интересным, успешным и таким, каким вы хотите. И, конечно же, я буду стараться рассказывать что-то новое про HTML и CSS.
Всех с Новым годом!
Полная разметка
<body>
<div class="congrats">
<div class="congrats__attention">
<span class="congrats__name">Екатерина,</span>
<span class="congrats__new-year">c Новым годом!</span>
</div>
<div class="congrats__wish">
<p class="congrats__message">Я желаю тебе удачи и успехов. Пусть новый год будет таким, каким ты хочешь!</p>
<p class="congrats__author">Константин Мельников, Декабрь 2025</p>
</div>
</div>
<div class="snowbank"></div>
<div class="snow snow_1"></div>
<div class="snow snow_2"></div>
<div class="snow snow_3"></div>
<div class="snow snow_4"></div>
<div class="snow snow_5"></div>
<div class="snow snow_6"></div>
<div class="snow snow_7"></div>
<div class="snow snow_8"></div>
<div class="snow snow_9"></div>
<div class="snow snow_10"></div>
<div class="snow snow_11"></div>
<div class="snow snow_12"></div>
<div class="snow snow_13"></div>
<div class="snow snow_14"></div>
<div class="snow snow_15"></div>
<div class="snow snow_16"></div>
<div class="snow snow_17"></div>
<div class="snow snow_18"></div>
<div class="snow snow_19"></div>
</body>Весь CSS
@import url('https://fonts.googleapis.com/css2?family=Marck+Script&display=swap');
body {
background-color: #b4dce4;
margin: 0;
min-height: max(500px, 100dvh);
font-family: "Marck Script", system-ui;
font-size: 1.25rem;
color: #668f9a;
display: grid;
grid-template-rows: 1fr 20dvh;
overflow: hidden;
}
@media (width > 1200px) {
body {
font-size: 2rem;
}
}
p[class] {
margin-block: 0;
}
.congrats {
margin: auto;
}
/*
=====
ПОЗДРАВЛЕНИЕ
=====
*/
@media (width < 480px) {
.congrats {
text-align: center;
padding: 1rem;
}
.congrats__message {
text-wrap: pretty;
}
}
.congrats__wish {
margin-top: 1.75rem;
display: grid;
gap: 3rem;
}
.congrats__attention {
display: grid;
font-size: 1.75rem;
}
.congrats__author {
display: flex;
align-items: center;
font-size: .75em;
justify-self: end;
}
.congrats__author::before {
content: "";
width: 25px;
height: 2px;
margin-right: .5em;
background-color: currentColor;
}
@media (width > 480px) {
.congrats__attention {
grid-template-columns: min-content 1fr;
grid-template-rows: repeat(2, 1fr);
line-height: 1;
width: fit-content;
margin-inline: auto;
}
.congrats__new-year {
grid-column: 2;
grid-row: 2;
}
.congrats__message {
max-width: 75%;
margin-inline: auto;
text-align: center;
}
}
@media (width > 1200px) {
.congrats__attention {
font-size: 4.5rem;
}
}
/*
=====
СНЕГ
=====
*/
.snowbank {
width: 100%;
height: 10dvh;
background-color: #fdfdff;
position: relative;
margin-top: auto;
animation: snowbank 30s linear forwards;
}
@keyframes snowbank {
0%, 28%{
height: 10dvh;
}
30%, 63%{
height: 13dvh;
}
65%, 100%{
height: 20dvh;
}
}
.snowbank::before,
.snowbank::after {
content: "";
height: 15vh;
aspect-ratio: 2;
border-radius: 50%;
background-color: inherit;
position: absolute;
top: -6dvh;
animation: snowdrifts 30s linear forwards;
}
.snowbank::before{
left: 0;
}
.snowbank::after{
right: 0;
}
@media (width > 1200px) {
.snowbank::before{
left: 10%;
}
.snowbank::after{
right: 10%;
}
}
@keyframes snowdrifts {
0%, 28%{
height: 15dvh;
top: -6dvh;
}
30%, 63%{
height: 16dvh;
top: -7dvh;
}
65%, 100%{
height: 17dvh;
top: -8dvh;
}
}
/*
=====
СНЕЖИНКИ
=====
*/
.snow::before,
.snow::after {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #fefefe;
opacity: 0;
animation-name: snow;
animation-duration: 8000ms;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
position: absolute;
top: 0;
}
.snow_1::before {
left: 10%;
translate: 0 1dvh;
}
.snow_1::after {
left: 20%;
translate: 0 1.5dvh;
animation-delay: 200ms;
}
.snow_2::before {
left: 30%;
translate: 0 2dvh;
animation-delay: 520ms;
}
.snow_2::after {
left: 40%;
translate: 0 2.5dvh;
animation-delay: 999ms;
}
.snow_3::before {
left: 50%;
translate: 0 3dvh;
animation-delay: 150ms;
}
.snow_3::after {
left: 60%;
translate: 0 3.5dvh;
animation-delay: 400ms;
}
.snow_4::before {
left: 70%;
translate: 0 4dvh;
animation-delay: 1550ms;
}
.snow_4::after {
left: 80%;
translate: 0 4.5dvh;
animation-delay: 655ms;
}
.snow_5::before {
left: 86%;
translate: 0 5dvh;
animation-delay: 250ms;
}
.snow_5::after {
left: 55%;
translate: 0 5.5dvh;
animation-delay: 600ms;
}
.snow_6::before {
left: 15%;
translate: 0 6dvh;
animation-delay: 2000ms;
}
.snow_6::after {
left: 25%;
translate: 0 6.5dvh;
animation-delay: 2400ms;
}
.snow_7::before {
left: 35%;
translate: 0 7dvh;
animation-delay: 2999ms;
}
.snow_7::after {
left: 45%;
translate: 0 7.5dvh;
animation-delay: 1300ms;
}
.snow_8::before {
left: 55%;
translate: 0 8dvh;
animation-delay: 2150ms;
}
.snow_8::after {
left: 65%;
translate: 0 8.5dvh;
animation-delay: 2140ms;
}
.snow_9::before {
left: 75%;
translate: 0 5.5dvh;
animation-delay: 2600ms;
}
.snow_9::after {
left: 84%;
translate: 0 5.5dvh;
animation-delay: 2666ms;
}
.snow_10::before {
left: 89%;
translate: 0 5.5dvh;
animation-delay: 2200ms;
}
.snow_10::after {
left: 10%;
translate: 0 5.5dvh;
animation-delay: 2400ms;
}
.snow_11::before {
left: 20%;
translate: 0 5.5dvh;
animation-delay: 3200ms;
}
.snow_11::after {
left: 30%;
translate: 0 5.5dvh;
animation-delay: 3500ms;
}
.snow_12::before {
left: 40%;
translate: 0 2.5dvh;
animation-delay: 3999ms;
}
.snow_12::after {
left: 50%;
translate: 0 3dvh;
animation-delay: 3150ms;
}
.snow_13::before {
left: 60%;
translate: 0 3.5dvh;
animation-delay: 3400ms;
}
.snow_13::after {
left: 70%;
translate: 0 4dvh;
animation-delay: 4550ms;
}
.snow_14::before {
left: 79%;
translate: 0 4.5dvh;
animation-delay: 3655ms;
}
.snow_14::after {
left: 86%;
translate: 0 5dvh;
animation-delay: 3250ms;
}
.snow_15::before {
left: 55%;
translate: 0 5.5dvh;
animation-delay: 4600ms;
}
.snow_15::after {
left: 15%;
translate: 0 6dvh;
animation-delay: 6000ms;
}
.snow_16::before {
left: 25%;
translate: 0 6.5dvh;
animation-delay: 6400ms;
}
.snow_16::after {
left: 35%;
translate: 0 7dvh;
animation-delay: 6999ms;
}
.snow_17::before {
left: 45%;
translate: 0 7.5dvh;
animation-delay: 5300ms;
}
.snow_17::after {
left: 55%;
translate: 0 8dvh;
animation-delay: 6150ms;
}
.snow_18::before {
left: 65%;
translate: 0 8.5dvh;
animation-delay: 6140ms;
}
.snow_18::after {
left: 75%;
translate: 0 5.5dvh;
animation-delay: 6600ms;
}
.snow_19::before {
left: 83%;
translate: 0 5.5dvh;
animation-delay: 6666ms;
}
.snow_19::after {
left: 89%;
translate: 0 5.5dvh;
animation-delay: 6200ms;
}
@keyframes snow {
0% {
translate: 0 0;
}
10% {
opacity: 1;
}
100% {
translate: 0 105dvh;
scale: 1.2;
}
}
На этом всё. Спасибо за чтение!
© 2025 ООО «МТ ФИНАНС»

