Семантическая вёрстка означает, что элемент выбирают по смыслу содержимого, а не по внешнему виду. Браузер переносит этот смысл в дерево доступности, поэтому скринридер может переходить между ориентирами и заголовками, поисковики понимают структуру, а нативные элементы дают нужное поведение бесплатно.
Ориентиры (landmarks): <header>, <nav>, <main>, <aside> и <footer> становятся областями, которые пользователь скринридера может вывести списком и перейти к нужной; <main> на странице один.
Заголовки: от <h1> до <h6> они образуют оглавление, по которому перемещается большинство пользователей скринридеров. Уровень выбирают по структуре, а размер задают в CSS.
**<article> содержит самостоятельный фрагмент (пост, комментарий, карточку товара); <section> обозначает тематическую часть страницы со своим заголовком.
Поведение**: <button> получает фокус и срабатывает на Enter и Space, <a href> можно открыть в новой вкладке, клик по <label> ставит фокус в его поле. <div> ничего из этого не умеет.
<div> и <span> по-прежнему подходят для обёрток, которые нужны только для раскладки.
ReactJunior
HTML и CSS
Семантический HTML, блочная модель, display и позиционирование, единицы, Flexbox и Grid, схлопывание отступов, адаптивность и container queries, CSS-переменные и темы, CSS Modules, Tailwind и CSS-in-JS
Закрыто вопросов
0 / 18
- 0Повторить
- 11Часто спрашивают
Вопросы
Каждый элемент состоит из четырёх слоёв: содержимое, padding, рамка (border) и margin. При значении по умолчанию content-box свойство width задаёт только содержимое, поэтому .card занимает 344 px (300 + 2 × 20 + 2 × 2); при border-box в width входят padding и рамка, карточка получается шириной 300 px, а содержимое сжимается до 256 px.
Margin лежит снаружи: отступы по 16 px отодвигают соседей, но в ширину не входят, а вертикальные margin соседних блоков могут схлопнуться в один.
**Зачем border-box**: width: 100% вместе с padding больше не вылезает за родителя, а изменение padding не меняет размер карточки.
Большинство проектов включает его для всех элементов в CSS-ресете, как в примере ниже; Preflight в Tailwind делает то же самое.
С height всё так же; outline и box-shadow места не занимают вовсе.
Блочный элемент начинается с новой строки, занимает всю ширину родителя и принимает width, height и все margin. Строчный элемент располагается внутри строки текста, по ширине равен содержимому и игнорирует width, height и вертикальные margin. inline-block стоит в строке, но размеры принимает как блок.
По умолчанию: div, p, h1, ul и section блочные; span, a, strong и code строчные. img тоже строчный, но как замещаемый элемент принимает width и height; button и input по умолчанию inline-block.
Padding у строчных: горизонтальный padding раздвигает соседей; вертикальный рисует фон, но не сдвигает соседние строки.
Зазоры: пробел или перенос строки между inline-block-элементами в HTML превращается в зазор около 4 px, а строчный img стоит на базовой линии текста, и под ним остаётся несколько пикселей, которые убирает display: block.
Сегодня ряды элементов обычно раскладывает flex- или grid-родитель, и тогда раскладку определяет display родителя.
display не меняет правил HTML: <div> внутри <p> остаётся невалидной разметкой.
display: none убирает элемент из раскладки и из дерева доступности. visibility: hidden сохраняет его место, но прячет от глаз, кликов и скринридеров. opacity: 0 только делает элемент прозрачным: место остаётся, клики и фокус он по-прежнему получает, и скринридер его читает.
Раскладка: только при display: none соседи занимают освободившееся место.
Клики: на элемент с opacity: 0 всё ещё можно кликнуть и перейти на него табом; это классический невидимый слой, который перехватывает клики.
Анимация: opacity меняется плавно, а display переключается мгновенно, поэтому при скрытии анимируют opacity, а visibility переключают в конце.
Дочерние элементы: внутри родителя с visibility: hidden потомок может задать visibility: visible и снова появиться; при двух других способах не может.
Только для скринридеров: подпись иконочной кнопки прячут классом visually-hidden, а не одним из этих трёх способов.
static означает обычный поток; relative остаётся в потоке и только визуально сдвигается на top и left; absolute выпадает из потока и позиционируется относительно ближайшего позиционированного предка; fixed относительно вьюпорта; sticky прокручивается как обычно, пока не дойдёт до отступа top, а потом прилипает внутри родителя.
Позиционированный предок: ближайший предок, у которого position не static; поэтому родителю ставят position: relative без смещений.
**relative сохраняет исходное место, так что сдвиг не толкает соседей.
fixed, который уезжает при прокрутке**: предок с transform или filter становится его содержащим блоком, и модалка внутри анимированной карточки двигается вместе с ней. Помогает портал в body.
**sticky, который не прилипает**: ему нужен top, предок с overflow: hidden или auto становится областью, внутри которой он прилипает, а за пределы родителя он не выходит.
Блоки с absolute сжимаются по содержимому, если не задать ширину; inset: 0 растягивает такой блок на весь содержащий блок.
px фиксирован, em считается от размера шрифта элемента (внутри font-size от родительского), rem от размера шрифта корня, % от размера родителя, а vh и dvh от высоты вьюпорта. Для шрифтов используйте rem: тогда текст следует размеру шрифта, который пользователь выбрал в браузере.
**Почему не px для текста**: если увеличить шрифт по умолчанию в настройках браузера, текст в rem вырастет, а в px нет. Масштаб страницы (zoom) увеличивает оба.
**em накапливается**: 1.2em внутри 1.2em даёт 1.44 от базового размера; это удобно для padding, который растёт вместе с текстом кнопки, и мешает во вложенных размерах шрифта.
**%**: width, padding и даже вертикальный margin считаются от ширины родителя; height: 50% работает, только если у родителя задана высота.
**vh на телефонах**: 100vh равен высоте при скрытой панели браузера, поэтому полноэкранный блок обрезается, пока панель видна. dvh следует за панелью, svh и lvh дают наименьшую и наибольшую высоту; поддержка во всех основных браузерах с 2022 года.
Плавная типографика: clamp(1.75rem, 1.2rem + 2vw, 2.5rem) растёт вместе с экраном в заданных пределах.
Сделать родителя гридом с place-items: center или flex-контейнером с justify-content: center и align-items: center. Оба способа центрируют дочерний элемент любого размера по обеим осям, не зная его размеров.
Только по горизонтали: margin-inline: auto у блока с заданной шириной, text-align: center для строчного содержимого вроде текста.
**margin: auto** внутри flex- или grid-родителя центрирует элемент по обеим осям; margin-left: auto отодвигает один элемент тулбара в конец.
Поверх другого содержимого: position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%), классический приём для блока неизвестного размера.
Обычный блок: align-content: center теперь центрирует содержимое по вертикали без flex и grid (во всех основных браузерах с 2024 года).
Нужно место: вертикальное центрирование ничего не даёт, пока родитель не выше ребёнка, например с min-height: 100dvh.
vertical-align: middle работает для строчного содержимого и ячеек таблицы, поэтому на div ничего не делает.
Псевдокласс (:hover, :focus-visible, :nth-child()) выбирает существующий элемент в определённом состоянии или позиции; псевдоэлемент (::before, ::after, ::placeholder) стилизует часть, которая не является DOM-элементом. :has() выбирает элемент по его содержимому, так что CSS может стилизовать родителя по его детям.
Синтаксис: одно двоеточие у псевдоклассов, два у псевдоэлементов; старые вроде :before работают и с одним.
**::before и ::after** появляются только при заданном content, хотя бы "". Они подходят для декора вроде стрелок; их текст нельзя выделить, поэтому настоящее содержимое остаётся в HTML.
Что стоит знать: :focus-visible показывает рамку фокуса только пользователям клавиатуры, :user-invalid помечает поле невалидным только после того, как пользователь с ним поработал, :is() и :where() группируют селекторы.
**:has()**: .card:has(img) стилизует карточки с картинкой, form:has(:invalid) .submit приглушает кнопку; это заменяет скрипты, которые только переключали класс у родителя. Поддержка во всех основных браузерах с декабря 2023 года.
Вертикальные margin соседних блоков не складываются, а сливаются в один, равный большему из них, а верхний margin первого ребёнка может «вытечь» сквозь родителя. Блочный контекст форматирования (BFC) означает изолированную область раскладки: margin не схлопываются через её границу, а плавающие элементы остаются внутри.
Соседи: margin-bottom: 24px над margin-top: 16px дают 24 px, а не 40 px. Горизонтальные margin не схлопываются никогда.
Родитель и ребёнок: margin-top заголовка в карточке без padding и рамки сдвигает вниз всю карточку, за пределы её фона. Padding, рамка или BFC на карточке удерживают margin внутри.
Без схлопывания: между flex- и grid-элементами, у float и абсолютно позиционированных элементов, а также через границу BFC.
Что создаёт BFC: display: flow-root (придуман ровно для этого), overflow со значением, отличным от visible и clip, float, абсолютное позиционирование, inline-block, а также flex- и grid-элементы.
Float: родитель с BFC растягивается и вмещает плавающих детей, это заменило старый хак clearfix.
На практике: gap или margin только в одну сторону не оставляют ничего для схлопывания.
flex-basis задаёт исходный размер элемента вдоль главной оси, flex-grow его долю оставшегося свободного места, а flex-shrink то, сколько он уступит, если элементы не помещаются. flex: 1 означает 1 1 0%: каждый элемент стартует с нуля, и вся строка делится поровну.
По умолчанию: 0 1 auto: элементы сохраняют размер по содержимому, не растут и сжимаются, если приходится.
**flex: 1 против flex: auto**: flex: auto (1 1 auto) стартует с размера содержимого и делит только остаток, поэтому элемент с длинной подписью шире; flex: 1 даёт двум элементам в строке шириной 600 px по 300 px, какой бы ни был текст.
Сжатие зависит от размера: нехватка места распределяется пропорционально flex-shrink, умноженному на базовый размер, поэтому широкий элемент отдаёт больше пикселей.
**flex: none** (0 0 auto) или flex-shrink: 0 не даёт сжать иконку или кнопку.
**Нижняя граница min-width: auto**: элемент не сжимается уже самого длинного слова или неразрывного URL, пока не задать min-width: 0; это обычная причина, по которой текст вылезает из flex-строки.
.info является flex-элементом со значением по умолчанию min-width: auto, поэтому не может сжаться меньше своего содержимого, а при nowrap содержимое равно полному имени файла. .name никогда не бывает уже текста, многоточию нечего обрезать, и вместо этого переполняется вся строка. Исправление: min-width: 0 на .info.
Почему многоточие не срабатывает: text-overflow работает, только когда элемент уже своего текста, а .name шириной с .info, который шириной с текст.
Почему без обёртки работает: те же три строки на прямом flex-элементе обрезают текст, потому что у элемента с overflow, отличным от visible, минимальный размер равен 0. Ловушка именно в лишнем div.
Исправление: min-width: 0 на том flex-элементе, который содержит текст. В колонке это min-height: 0; без него прокручиваемая панель внутри flex-колонки обычно и отказывается прокручиваться.
Кнопки: flex-shrink: 0 сохраняет их полный размер, когда имя начинает уступать место.
В Grid та же нижняя граница: 1fr означает minmax(auto, 1fr), и там исправление выглядит как minmax(0, 1fr).
fr означает долю места, оставшегося после фиксированных треков, minmax(min, max) позволяет треку меняться между двумя размерами, а repeat() записывает список треков один раз. С auto-fill или auto-fit он создаёт столько колонок, сколько помещается; auto-fit затем схлопывает пустые, и немногие элементы растягиваются на всю строку.
**fr считается последним**: в 200px 1fr 2fr на гриде шириной 800 px оставшиеся 600 px делятся на 200 и 400.
**У 1fr есть нижняя граница**: он означает minmax(auto, 1fr), поэтому длинное слово держит колонку широкой, и две колонки 1fr получаются неравными; minmax(0, 1fr) делает их равными.
**auto-fill против auto-fit**: три карточки при месте на пять колонок по 200 px остаются шириной 200 px с auto-fill, который сохраняет пустые треки; с auto-fit они делят всю ширину. Заполненная строка выглядит одинаково в обоих случаях.
Без медиазапросов: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); внутренний min() не даёт переполнения на экранах уже 240 px.
Именованные области: grid-template-areas описывает раскладку текстом, поэтому медиазапрос может перестроить её, не трогая HTML. Держите визуальный порядок близким к порядку в DOM: по нему идут пользователи клавиатуры.
Custom properties (--accent: #2563eb, чтение через var(--accent)) являются настоящими значениями CSS в браузере: они наследуются вниз по DOM, переопределяются для элемента или медиазапроса и меняются во время работы. Переменные Sass заменяются фиксированными значениями при сборке, и в итоговом CSS их нет.
Темы: токены на :root, новые значения под [data-theme="dark"], и каждый компонент с var(--surface) переключается без дополнительных правил. Атрибут берут из сохранённого выбора или из prefers-color-scheme.
Область действия: --card-padding, заданный на одном контейнере, меняет только его поддерево; это настройка компонента без новых классов.
Запасные значения: var(--card-padding, 16px) выручает, если переменная не определена; значение, недопустимое для свойства, делает свойство unset, а не возвращает предыдущее правило.
**color-scheme: dark** делает тёмными ещё и полосы прокрутки и элементы форм; light-dark(#fff, #111) выбирает значение по активной схеме (во всех основных браузерах с 2024 года).
Без вспышки: ставьте атрибут на <html> крошечным inline-скриптом в <head> до первой отрисовки. Изменение переменной из JavaScript меняет оформление страницы без ре-рендера React.
Mobile-first означает, что базовые стили написаны для самого узкого экрана, а блоки @media (min-width: 768px) добавляют колонки по мере роста ширины. Медиазапросы проверяют вьюпорт, а container queries ширину контейнера компонента, поэтому карточка подстраивается, куда бы её ни поставили.
Тег viewport: без <meta name="viewport" content="width=device-width, initial-scale=1"> телефон раскладывает страницу шириной около 980 px и уменьшает её, так что мобильные стили не применяются.
Почему mobile-first: каждая точка перелома (breakpoint) только добавляет к простой раскладке, а не отменяет десктопные правила. Ставьте breakpoints там, где ломается контент, а не под ширины устройств.
Сначала резиновость: flex-wrap, Grid с auto-fit и шрифты на clamp() убирают большинство breakpoints.
Container queries: container-type: inline-size на обёртке, @container (min-width: 420px) в компоненте. Одна и та же карточка становится горизонтальной в широкой колонке и остаётся вертикальной в сайдбаре, а медиазапрос эти случаи не различает. Поддержка везде с 2023 года.
Ограничение: элемент не может запросить собственный размер, а ширина контейнера больше не может зависеть от содержимого, поэтому обёртка с шириной по содержимому схлопывается.
srcset перечисляет одно изображение в нескольких ширинах, а sizes сообщает, какой ширины оно будет на странице, и браузер выбирает самый маленький файл, который выглядит чётко на этом экране. <picture> меняет файл или формат по условию. Атрибуты width и height резервируют место до загрузки.
Дескрипторы ширины: при файлах 480w и 960w и sizes="100vw" телефону шириной 400 px с экраном 2x нужно 800 пикселей, и он получает файл 960.
**Без sizes** браузер считает, что картинка шириной 100vw, и миниатюра в сетке скачивает полноэкранный файл. Опишите раскладку: sizes="(min-width: 1024px) 33vw, 100vw".
**<picture>**: <source type="image/avif"> перед запасным <img> отдаёт новые форматы там, где они поддерживаются; <source media> даёт телефонам другое кадрирование. Внутренний <img> обязателен и несёт alt.
Без сдвига раскладки: атрибуты заранее сообщают пропорции, а CSS width: 100%; height: auto по-прежнему масштабирует картинку.
Загрузка: loading="lazy" ниже первого экрана, но никогда для главной картинки (hero): ей ставят fetchpriority="high", потому что обычно именно она становится элементом LCP.
transition анимирует свойство от старого значения к новому, когда оно меняется, например на :hover или при переключении класса. Анимация на @keyframes сама проигрывает заданную последовательность, может повторяться и не требует смены состояния. Анимируйте transform и opacity: они обходятся без layout и paint.
Transitions знают два состояния: transition: transform 200ms ease-out на элементе, новое значение под .is-open. Если прервать переход на середине, он плавно развернётся с текущей точки.
Keyframes подходят для лоадеров, пульсации и появления: animation: spin 1s linear infinite.
Дорогие свойства: width, height, top и margin перезапускают layout каждый кадр; сдвигайте через translate и увеличивайте через scale.
**will-change: transform заранее выносит элемент в отдельный слой; ставьте его на немногие элементы, потому что каждый слой стоит памяти GPU.
Уменьшение движения**: отключайте крупные движения под @media (prefers-reduced-motion: reduce).
**Из display: none**: @starting-style задаёт первый кадр, поэтому элемент может плавно появиться при показе (во всех основных браузерах с 2024 года). Плавное скрытие до display: none через transition-behavior: allow-discrete работает в Chrome и Safari, но пока не в Firefox.
Обычный CSS глобален, поэтому каждый подход по-своему избегает конфликтов: BEM через именование, CSS Modules через переименование классов каждого файла при сборке, Tailwind через утилитарные классы вместо имён, CSS-in-JS через генерацию стилей в компоненте. Платить JavaScript-кодом в браузере приходится только за runtime CSS-in-JS.
BEM (.card__title--active): не нужны инструменты и специфичность плоская, но соблюдается он только дисциплиной.
CSS Modules: styles.button при сборке превращается во что-то вроде Button_button__x7a2. Обычный CSS, нулевой рантайм, встроено в Vite и Next.js; чтобы стилизовать дочерний компонент снаружи, нужен проп className.
Tailwind: утилиты в разметке из фиксированной шкалы, и в CSS попадают только использованные классы. Цена: длинные списки классов; clsx и tailwind-merge справляются с условными и конфликтующими классами.
Runtime CSS-in-JS (styled-components, Emotion): стили могут читать пропсы и тему, но CSS собирается и вставляется во время рендера, библиотека увеличивает бандл, а серверный рендеринг требует дополнительной настройки.
Zero-runtime CSS-in-JS (vanilla-extract, Panda CSS, StyleX): стили на TypeScript, скомпилированные в статический CSS.
Динамические значения: передавайте CSS-переменную через style, а не генерируйте класс под каждое значение.
Runtime CSS-in-JS превращает стили, написанные на JavaScript, в CSS во время рендера компонентов: библиотека едет в браузер, вставляет правила во время рендера и должна отправлять их вместе со стримящимся HTML. У Server Components нет контекста, и они стремятся не отправлять JavaScript, а эти библиотеки опираются и на то, и на другое.
Во что это обходится
Работа при рендере: каждый рендер стилизованного компонента сериализует его стили, хеширует их в имя класса и вставляет новые правила; color: ${(p) => p.color} создаёт класс под каждое значение.
Пересчёт стилей: каждое вставленное правило заставляет браузер пересчитывать стили. Документация React не советует вставлять <style> в рантайме и рекомендует статические CSS-файлы плюс inline-стили для динамических значений.
Стриминговый SSR: сервер должен отправить правила каждого чанка раньше его HTML (в App Router через реестр в Client Component с useServerInsertedHTML), иначе страница мигает без стилей.
Почему с Server Components хуже
ThemeProvider и реестр стилей живут в контексте React, который Server Components читать не могут, поэтому годами каждому файлу со styled-компонентами требовался "use client", и их код возвращался в бандл.
Начиная с 6.3 (январь 2026) styled-components рендерится в Server Components, выдавая inline-теги <style>, но ThemeProvider там ничего не делает, и темы переезжают на CSS-переменные. Emotion по-прежнему работает только в Client Components.
Что используют вместо этого
CSS Modules или обычный CSS с custom properties, а также Tailwind: статический CSS, собранный заранее, который работает и в Server Components.
Zero-runtime CSS-in-JS: vanilla-extract, Panda CSS, StyleX или Linaria компилируют типизированные стили в статический CSS.
Миграция: новый код пишут статическим способом, начинают с листовых компонентов, а стили, зависящие от пропсов, заменяют CSS-переменными.