Figma - подсказки
Ссылка на макет: Figma
Шаг 1 — Структура проекта и статическое превью
Вспоминаем пример структуры директорий, который приводился еще в первой лабораторке и воспроизводим её.
- index.html
-
-
- style.css
-
- logo.jpg
-
- Gerbera.otf
-
Шаг 2 — CSS-переменные в :root + шрифт
CSS-переменные (custom properties) наследуются и пересчитываются в рантайме. Их можно использовать как единый источник правды для интерфейса: меняете значение один раз — меняются все элементы их использующие. Благодаря этому дальше шапка, кнопки, форма и адаптив будут консистентны.
:root {
--white: #FFFFF8; --white-50: #FFFFF880;
--background: #1B1C21; --tonal: #313237; --tonal-hover: #49494C;
--border-20: rgba(255,255,248,.20); --border-40: rgba(255,255,248,.40);
--red: #FF312E; --green: #3E885B; --control-border-width: 1px;
}
@font-face {
font-family: "Gerbera"; font-display: swap; font-style: normal;
src: url("../fonts/Gerbera.otf") format("opentype");
}
body { font-family: "Gerbera", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; }Note
Используйте фолбэк: color: var(--green, #3E885B);. Переменные можно менять на лету:
document.documentElement.style.setProperty('--background', '#000');.
Шаг 3 — Оформление стилей: методология BEM
BEM — это Block__Element–Modifier:
блок (header, info, btn) → элемент (header__nav, info__title) → модификатор (btn--main, txt--inactive).
Этим мы исключаем «накладки» стилей между компонентами и держим низкую специфичность.
<button class="btn btn--main">Отправить</button> <!-- Блок + модификатор -->Tip
Пишите «плоские» селекторы (.header__nav-link) вместо каскадов (.header .nav a). Все следующие блоки (шапка, секции, форма, меню) будут использовать одни и те же правила именования.
Шаг 4 — Сброс и установка базовых стилей
установим базовыое значнеие box-sizing, чтобы наш параметр width вкючал в себя отступы вокруг элемента, и включаем явный фокус для клавиатурной навигации. Это пригодится и в форме, и в мобильном меню.
html { box-sizing: border-box; }
*, *::before, *::after { box-sizing: inherit; }
:focus-visible { outline: 2px solid var(--border-40); outline-offset: 3px; }Caution
Фокус один из основных требования WСAG 2.1. (стандарты дотсупности), да и просто с ним проще и удобнее использовать табуляцию при перемещении по сайту.
Шаг 5 — Desktop-first разметка
Desktop-first предполагает, что сначала мы проводим полную десктоп компоновку.
.container { width: 1200px; margin-inline: auto; padding-inline: 24px; }
@media (max-width: 1279px) { .container { width: 100%; } }Шаг 6 — Шапка и навигация
Навигация по странице организуется при помощи якорей вида href="#skills", где skills — id целевой секции. Добавим гладкую прокрутку и компенсацию высоты фикс-шапки.
html { scroll-behavior: smooth; }Tip
Чтобы якорь попадал в нужное место, в каждой секции задавайте корректный id.
После якорей зафиксируем семантическую иерархию заголовков.
Шаг 7 — Главный заголовок и иерархия
Один <h1> на страницу, ниже — <h2> секций и <h3> подразделов. Это улучшает доступность и читабельность документа для автоматических анализаторов.
<h1 class="content__title h-1">Название страницы</h1>Note
Внутри секций «О себе», «Навыки», «Образование» придерживаемся той же логики; это облегчает дальнейшую стилизацию и работу ридеров.
Шаг 8 — Основной контент страницы
Семантика + BEM помогают масштабировать стили. Остальные секции («Навыки», «Образование») верстаются по тому же шаблону, меняются только содержимое и сетка.
<section id="about" class="info info--general r-16">
<h2 class="info__title r-16">О себе</h2>
<div class="info__content">
<p>Короткий абзац о себе...</p>
</div>
</section>id="about"— якорь для меню из Шага 6.info— блок,info__title,info__content— элементы;info--general— модификатор.- Для «Навыки» используйте сетку 2 колонки на десктопе; для «Образование» — 3 колонки (подробнее — в адаптиве).
Теперь добавим форму — один инпут как эталон, остальные по этому же паттерну.
Шаг 9 — Floating labels (поля ввода)
Плавающий лейбл экономит место и сохраняет контекст. Ошибку выводим в связанный контейнер, чтобы её озвучивали ридеры.
<div class="contact-form__group">
<input class="contact-form__control" type="email" id="email" name="email"
placeholder="Почта" required aria-describedby="err-email">
<label for="email" class="contact-form__label">Почта</label>
<p id="err-email" class="contact-form__error" aria-live="polite"></p>
</div>Tip
Поля name, tg, message создаются аналогично: те же классы, корректные type/pattern/minlength и уникальный aria-describedby.
Чтобы форма была дружелюбной, добавим короткую валидацию с «человечными» сообщениями.
Шаг 10 — Проверка ошибок при отправке формы
Сначала используем нативные атрибуты (required, type="email", pattern, minlength). Чтобы тексты были понятными, поверх добавим минимальную обработку сабмита.
<script>
const form = document.querySelector('.contact-form__form');
form?.addEventListener('submit', (e) => {
let firstInvalid = null;
for (const el of form.querySelectorAll('.contact-form__control')) {
const err = document.getElementById(el.getAttribute('aria-describedby'));
el.setCustomValidity('');
if (!el.checkValidity()) {
if (el.validity.valueMissing) el.setCustomValidity('Заполните это поле');
else if (el.type === 'email' && el.validity.typeMismatch) el.setCustomValidity('Некорректный email');
err.textContent = el.validationMessage;
firstInvalid ??= el;
} else err.textContent = '';
}
if (firstInvalid) { e.preventDefault(); firstInvalid.reportValidity(); firstInvalid.focus(); }
});
</script>Note
Такой же принцип применим к остальным полям: однообразная проверка, вывод в связанный .contact-form__error (см. один показательный пример выше).
Когда десктоп готов, логично перейти к адаптиву: «сужаем» сетки и поведение.
Шаг 11 — Добавляем адаптив
Берём сетки и уменьшаем количество колонок, уменьшаем отступы, скрываем десктоп-навигацию в пользу мобильной панели.
/* tablet */
@media (max-width: 1024px) {
.info--skills .info__content { grid-template-columns: 1fr; } /* 2 → 1 */
.info__details { grid-template-columns: 1fr 1fr; } /* 3 → 2 */
.container { padding-inline: 20px; }
}
/* mobile */
@media (max-width: 640px) {
.info__details { grid-template-columns: 1fr; } /* 2 → 1 */
.container { padding-inline: 16px; }
}Tip
Точно так же вы обработаете любой похожий блок: repeat(3, 1fr) → repeat(2, 1fr) → 1fr. Для типографики/отступов можно применить clamp().
Остаётся мобильная навигация: off-canvas вместо десктопного списка.
Шаг 12 — Мобильное меню
Принцип в трёх шагах:
- Кнопка-бургер (
aria-controls="mobile-nav",aria-expandedменяется). - Панель
#mobile-nav+ подложка; классis-openвключает сдвиг. - Мини-скрипт: клики по кнопке/подложке,
Escape, блокировка прокруткиbody, фокус на первый пункт, фокус-трап.
const burger = document.querySelector('.header__burger');
const panel = document.getElementById('mobile-nav');
const backdrop = document.querySelector('.mobile-nav__backdrop');
function toggle(open){
panel.classList.toggle('is-open', open);
burger.setAttribute('aria-expanded', String(open));
document.body.style.overflow = open ? 'hidden' : '';
}
burger?.addEventListener('click', () => toggle(!panel.classList.contains('is-open')));
backdrop?.addEventListener('click', () => toggle(false));
document.addEventListener('keydown', e => e.key === 'Escape' && toggle(false));Note
Ссылки внутри меню (.mobile-nav__link) закрывают панель по тому же правилу: при клике вызывайте toggle(false). Фокус-трап добавляется аналогичным минимумом кода при необходимости.