gitaspen docs

Стиль кода фронтенда

Как выглядит код внутри одного файла компонента: структура разметки, вложенность стилей, имена, комментарии, приёмы взаимодействия. Раскладка по слоям, границы импортов и правило «стиль лежит рядом с компонентом» — в BMFP. Роли цвета, типографика, обязательные состояния экрана — в правилах дизайна, единицы и точки перестроения — в адаптивности. Здесь только то, что решается внутри файла.

7 минут

Как выглядит код внутри одного файла компонента: структура разметки, вложенность стилей, имена, комментарии, приёмы взаимодействия. Раскладка по слоям, границы импортов и правило «стиль лежит рядом с компонентом» — в BMFP. Роли цвета, типографика, обязательные состояния экрана — в правилах дизайна, единицы и точки перестроения — в адаптивности. Здесь только то, что решается внутри файла.

Место в цепочке

Откуда пришлиЭтот документКуда ведёт
код разложен по слоям, стиль лежит рядом с компонентом (BMFP); токены темы заданы (правила дизайна)как пишется отдельный компонент: разметка, вложенность стилей, имена, взаимодействиеревью и тестирование: поведение проверяется через границу компонента

Что предыдущий этап обязан обеспечить: алиасы слоёв в конфигурации сборки и типов, набор токенов темы (цвет, отступ, радиус, типографика). Без токенов правила ниже вырождаются в спор о конкретных значениях в каждом файле.

Что этот документ оставляет следующему: компоненты, у которых наблюдаемое поведение — клик, наведение, фокус — задано в одном месте и проверяется без знания внутренней разметки.


Разметка

Обёртка заводится под задачу. Элемент-контейнер оправдан, когда он задаёт раскладку, ограничивает область или служит границей группы. Обёртка «на всякий случай» удлиняет дерево: каждый уровень — ещё одна точка, где протекает отступ и теряется контекст наложения.

Тег выбирается по роли. Ссылка — <a>, кнопка — <button>: клавиатура, фокус и контекстное меню браузера работают без дополнительного кода. Минимум по доступности — в правилах дизайна.

Кликабельная карточка целиком

Задача: вся площадь карточки ведёт по ссылке, но в разметке остаётся одна ссылка, а не ссылка вокруг всего содержимого.

tsx
<div className={styles.card}>
  <a className={styles.link} href={href} aria-label={title} />
  <h3 className={styles.title}>{title}</h3>
  <p className={styles.text}>{text}</p>
</div>
scss
.card {
  position: relative;

  &:hover .title { color: var(--color-text-strong); }
  &:focus-within { outline: 2px solid var(--color-focus); }

  .link {
    position: absolute;
    inset: 0;
    z-index: 1;
  }

  .title,
  .text {
    position: relative;
    z-index: 2;
    pointer-events: none;
  }
}

Из чего складывается приём:

  • ссылка пустая и растянута по карточке (position: absolute; inset: 0) — нажатие засчитывается на всей площади, а не только на тексте заголовка;
  • карточке нужен position: relative: без него ссылка растянется по ближайшему позиционированному предку выше и перекроет чужую область;
  • содержимое лежит слоем выше (z-index: 2) и не перехватывает указатель (pointer-events: none) — иначе попадание в букву заголовка не считается попаданием в ссылку;
  • ссылке даётся доступное имя (aria-label или визуально скрытый текст): у пустой ссылки его нет, и для программы чтения с экрана она остаётся безымянной;
  • вложенный интерактивный элемент — вторая ссылка, кнопка «в избранное» — возвращает себе pointer-events: auto и слой выше остальных, иначе он недоступен.

Ограничение приёма: содержимое с pointer-events: none не выделяется мышью. Если выделение текста нужно, ссылкой делается заголовок, а карточка остаётся некликабельной.

Состояние — на родителе

Наведение, нажатие и фокус описываются на карточке, а не на вложенной ссылке:

scss
.card:hover { … }        /* да */
.card .link:hover { … }  /* нет */

Указатель физически находится над карточкой; ссылка лежит под содержимым и своего наведения может не получить. Правило действует и в общем виде: состояние описывается на том элементе, границы которого видит пользователь, а не на том, который технически принимает событие. Фокус с клавиатуры попадает на ссылку, поэтому видимое состояние вешается через :focus-within на карточку.


Стили

Вложенность повторяет структуру, а не углубляет её

scss
.panel {          // контейнер компонента
  .title { … }
  .list { … }
}

.card { … }       // переиспользуемый элемент — верхний уровень, со своей вложенностью

Собственные элементы компонента вкладываются в его класс — по файлу видно, из чего компонент состоит. Класс, который применяется и в других местах, остаётся на верхнем уровне: вложенный селектор работает только внутри родителя, и при переносе его придётся копировать.

Глубина ограничивается тремя уровнями. Селектор из пяти имён описывает не элемент, а конкретное дерево разметки и перестаёт работать при первой же его перестановке.

Свойства со значением по умолчанию не пишутся

Строка остаётся в файле, только если меняет поведение по сравнению с тем, что уже действует, — значением браузера по умолчанию или общим сбросом. Что удаляется чаще всего:

СтрокаКогда лишняя
font-style: normalпочти всегда: наклонный шрифт нужно включить, а не выключить
margin: 0, padding: 0если сброс уже снял их у этого элемента
text-decoration: noneна всём, кроме <a>
background: transparentу элемента без собственного фона
position: static, display: blockзначения по умолчанию для блочного элемента

Проверка: удалить строку и посмотреть на элемент. Ничего не изменилось — строка была лишней. Причина правила не в объёме файла: строка, которая ничего не меняет, читается как принятое решение, и при правке её обходят стороной, опасаясь сломать вид.

Слои

z-index задаётся парой соседних значений внутри одного контекста наложения: подложка — 1, содержимое — 2. Значения вида 999 означают, что нужный контекст не найден; лечится это добавлением position: relative общему родителю, а не увеличением числа.

Значения

Цвет, отступ, радиус, тень и типографика приходят из токенов темы (BMFP, правила дизайна). В файле компонента остаются только числа, описывающие его собственную геометрию: соотношение сторон, число колонок, доля ширины.


Именование

ЧтоПравилоПример
Классроль внутри компонентаtitle, list, link — не boldRed
Класс в модуле стилейcamelCasecardTitle доступен как styles.cardTitle; card-title требует styles['card-title']
Компонентчто это, а не откуда взялосьInfoCard — не SectionInfoLink
Тип свойствимя компонента + PropsCardProps
Свойство-обработчикпо событию снаружиonSelect — не handleDelete

Компонент, названный по месту первого использования, не переносится в другое место без переименования — а именно перенос и означает, что он оказался переиспользуемым.

Свойства компонента типизируются явно. Необязательное свойство получает значение по умолчанию в сигнатуре, а не проверку в теле: значение по умолчанию видно вместе с типом.


Комментарии

Комментарий объясняет причину или ограничение, а не действие:

scss
/* указатель снят: иначе клик по заголовку не доходит до ссылки под ним */
pointer-events: none;

Комментарий /* стили заголовка */ над .title пересказывает следующую строку и устаревает раньше неё.

Декоративные разделители (/* ===== Карточки ===== */) не заводятся: порядок и вложенность уже показывают структуру, а рамка из символов расходится с содержимым при первой перестановке блоков.

Закомментированный код в файле не остаётся — прежние варианты хранит история репозитория. Пометка TODO сопровождается условием снятия; без условия она остаётся навсегда.


Чеклист перед ревью

  • нет обёрток, у которых нет задачи;
  • интерактивная область — один элемент, у него есть доступное имя;
  • содержимое поверх ссылки не перехватывает указатель;
  • наведение и фокус описаны на видимой границе элемента;
  • вложенность стилей не глубже трёх уровней, переиспользуемый класс — на верхнем уровне;
  • нет свойств, совпадающих со значением по умолчанию;
  • z-index — соседние значения в одном контексте наложения;
  • цвета и отступы взяты из токенов;
  • комментарии отвечают на «почему»; закомментированного кода нет.
Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.