
Задача
Одностраничник для типографии на трёх файлах. Вместо картинок-баннеров — демонстрация самой типографики: каскад размеров, бегущая строка, спецификация сетки и живой образец, на котором видно, как работают трекинг и вес.
Как делал
Бегущая строка сделана в два span с одинаковым текстом внутри одного трека. Анимация сдвигает его на translateX(-50%) — ровно на ширину половины, поэтому второй span становится первым и цикл замыкается без единого разрыва. Один span со второй копией сделал бы рывок в конце каждого прохода.
.type__marquee-track {
display: inline-flex;
animation: marquee 20s linear infinite;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); } /* ширина ровно одного span из двух */
}
Образец в секции типографики реагирует на курсор без единой строки JS: letter-spacing переходит с 0 на 0.1em, и разрядка разъезжается прямо в глазах. Растянутый трекинг съел бы правый край, поэтому text-align: center и padding по вертикали страхуют от переполнения.
.type__specimen {
font-weight: 900;
font-size: clamp(5rem, 15vw, 12rem);
text-align: center;
padding: var(--space-section) 0;
transition: letter-spacing 0.4s ease;
user-select: none;
}
.type__specimen:hover { letter-spacing: 0.1em; }
Глитч на ссылках — четыре кадра text-shadow, где красный и голубой расходятся на пару пикселей и снова сходятся. Эффект держится 0.3 секунды на ховере, а не горит постоянно.
.glitch-link:hover { animation: glitch 0.3s ease; }
@keyframes glitch {
0% { text-shadow: 2px 0 var(--color-accent), -2px 0 cyan; }
25% { text-shadow: -2px 0 var(--color-accent), 2px 0 cyan; }
50% { text-shadow: 2px 2px var(--color-accent), -2px -2px cyan; }
75% { text-shadow: -2px 2px var(--color-accent), 2px -2px cyan; }
100% { text-shadow: none; }
}
Появление секций идёт через IntersectionObserver, но каскад внутри секции раздаёт сам обсервер: он находит всех детей с .reveal-child и проставляет каждому transitionDelay по индексу. Так задержки считаются от фактического DOM, а не пишутся инлайном в разметку.
const children = entry.target.querySelectorAll(`.reveal-child`);
children.forEach((child, index) => {
child.style.transitionDelay = `${index * 0.1}s`;
child.classList.add(`revealed`);
});
Фотографии приходят обесцвеченными и оживают на ховере: фон первого экрана — grayscale(1) на opacity: 0.06, карточки проектов — grayscale(0.6), снимок в «О себе» — grayscale(0.3). Чтобы одна страница не выглядела галереей случайных снимков, а держалась одного тона.
Итог
Лендинг работает на трёх файлах без зависимостей: HTML, CSS и семьдесят пять строк JS. Вся типографика — настоящая, а не картинка: Playfair Display для акцентов, DM Sans для текста, оба с Google Fonts.