Перейти к содержимому

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", где skillsid целевой секции. Добавим гладкую прокрутку и компенсацию высоты фикс-шапки.

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>

Теперь добавим форму — один инпут как эталон, остальные по этому же паттерну.


Шаг 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 — Мобильное меню

Принцип в трёх шагах:

  1. Кнопка-бургер (aria-controls="mobile-nav", aria-expanded меняется).
  2. Панель #mobile-nav + подложка; класс is-open включает сдвиг.
  3. Мини-скрипт: клики по кнопке/подложке, 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). Фокус-трап добавляется аналогичным минимумом кода при необходимости.