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

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

    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). Фокус-трап добавляется аналогичным минимумом кода при необходимости.