Стиль кода фронтенда
Как выглядит код внутри одного файла компонента: структура разметки, вложенность стилей, имена, комментарии, приёмы взаимодействия. Раскладка по слоям, границы импортов и правило «стиль лежит рядом с компонентом» — в BMFP. Роли цвета, типографика, обязательные состояния экрана — в правилах дизайна, единицы и точки перестроения — в адаптивности. Здесь только то, что решается внутри файла.
7 минутКак выглядит код внутри одного файла компонента: структура разметки, вложенность стилей, имена, комментарии, приёмы взаимодействия. Раскладка по слоям, границы импортов и правило «стиль лежит рядом с компонентом» — в BMFP. Роли цвета, типографика, обязательные состояния экрана — в правилах дизайна, единицы и точки перестроения — в адаптивности. Здесь только то, что решается внутри файла.
Место в цепочке
| Откуда пришли | Этот документ | Куда ведёт |
|---|---|---|
| код разложен по слоям, стиль лежит рядом с компонентом (BMFP); токены темы заданы (правила дизайна) | как пишется отдельный компонент: разметка, вложенность стилей, имена, взаимодействие | ревью и тестирование: поведение проверяется через границу компонента |
Что предыдущий этап обязан обеспечить: алиасы слоёв в конфигурации сборки и типов, набор токенов темы (цвет, отступ, радиус, типографика). Без токенов правила ниже вырождаются в спор о конкретных значениях в каждом файле.
Что этот документ оставляет следующему: компоненты, у которых наблюдаемое поведение — клик, наведение, фокус — задано в одном месте и проверяется без знания внутренней разметки.
Разметка
Обёртка заводится под задачу. Элемент-контейнер оправдан, когда он задаёт раскладку, ограничивает область или служит границей группы. Обёртка «на всякий случай» удлиняет дерево: каждый уровень — ещё одна точка, где протекает отступ и теряется контекст наложения.
Тег выбирается по роли. Ссылка — <a>, кнопка — <button>: клавиатура, фокус и контекстное
меню браузера работают без дополнительного кода. Минимум по доступности — в правилах дизайна.
Кликабельная карточка целиком
Задача: вся площадь карточки ведёт по ссылке, но в разметке остаётся одна ссылка, а не ссылка вокруг всего содержимого.
<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>.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 не выделяется мышью. Если выделение текста
нужно, ссылкой делается заголовок, а карточка остаётся некликабельной.
Состояние — на родителе
Наведение, нажатие и фокус описываются на карточке, а не на вложенной ссылке:
.card:hover { … } /* да */
.card .link:hover { … } /* нет */Указатель физически находится над карточкой; ссылка лежит под содержимым и своего наведения может
не получить. Правило действует и в общем виде: состояние описывается на том элементе, границы
которого видит пользователь, а не на том, который технически принимает событие. Фокус с клавиатуры
попадает на ссылку, поэтому видимое состояние вешается через :focus-within на карточку.
Стили
Вложенность повторяет структуру, а не углубляет её
.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 |
| Класс в модуле стилей | camelCase | cardTitle доступен как styles.cardTitle; card-title требует styles['card-title'] |
| Компонент | что это, а не откуда взялось | InfoCard — не SectionInfoLink |
| Тип свойств | имя компонента + Props | CardProps |
| Свойство-обработчик | по событию снаружи | onSelect — не handleDelete |
Компонент, названный по месту первого использования, не переносится в другое место без переименования — а именно перенос и означает, что он оказался переиспользуемым.
Свойства компонента типизируются явно. Необязательное свойство получает значение по умолчанию в сигнатуре, а не проверку в теле: значение по умолчанию видно вместе с типом.
Комментарии
Комментарий объясняет причину или ограничение, а не действие:
/* указатель снят: иначе клик по заголовку не доходит до ссылки под ним */
pointer-events: none;Комментарий /* стили заголовка */ над .title пересказывает следующую строку и устаревает
раньше неё.
Декоративные разделители (/* ===== Карточки ===== */) не заводятся: порядок и вложенность уже
показывают структуру, а рамка из символов расходится с содержимым при первой перестановке блоков.
Закомментированный код в файле не остаётся — прежние варианты хранит история репозитория. Пометка
TODO сопровождается условием снятия; без условия она остаётся навсегда.
Чеклист перед ревью
- нет обёрток, у которых нет задачи;
- интерактивная область — один элемент, у него есть доступное имя;
- содержимое поверх ссылки не перехватывает указатель;
- наведение и фокус описаны на видимой границе элемента;
- вложенность стилей не глубже трёх уровней, переиспользуемый класс — на верхнем уровне;
- нет свойств, совпадающих со значением по умолчанию;
-
z-index— соседние значения в одном контексте наложения; - цвета и отступы взяты из токенов;
- комментарии отвечают на «почему»; закомментированного кода нет.
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.