Свой сброс стилей: что браузер рисует без спроса

стили≈ 10 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Страница без единой строки CSS всё равно оформлена: заголовок крупный, у списка отступ слева, между абзацами просвет. Это стили самого браузера, и лежат они не только под пустой страницей — под каждым вашим правилом, на каждом сайте, который вы откроете. Обычно они помогают, но иногда подставляют: атрибут hidden перестаёт прятать, кнопка не берёт шрифт страницы, а отступ появляется там, где вы его не писали. Измерим стили браузера, найдём, что поверх них положил сам учебник, и соберём свой сброс — несколько строк, каждая из которых отвечает на измеренную проблему, а не на «так принято».

Разделы темы:·

Со стилями браузера вы уже встречались, когда разбирали смысловые теги и ориентиры: <section> там оказался блоком, потому что так записано в листе стилей браузера. Это набор обычных правил CSS, которые действуют на любой странице раньше ваших. Какими им быть, описывает стандарт HTML в разделе об отрисовке, и современные браузеры в главном ему следуют.

Проверим на странице, в которой нет ни одного <style>: заголовок, список из одного пункта и абзац. Прогноз: окажутся ли все четыре числа нулями — а если нет, то какими?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Ни одного нуля. Вот что записано в листе браузера и откуда взялись эти числа:
ЗамерВ листе браузераОткуда число
h1 → font-size: 28pxfont-size: 2em2 × 14px: в font-size единица em — размер шрифта родителя
h1 → margin-top: 18.76pxотступы сверху и снизу по 0.67em0.67 × 28px: в остальных свойствах em — размер шрифта самого элемента
ul → padding-left: 40pxвнутренний отступ слева 40pxпиксели от шрифта не зависят
p → margin-top: 14pxотступы сверху и снизу по 1em1 × 14px шрифта абзаца

Правила браузера спорят с вашими по общим законам, и среди обычных объявлений они самые слабые. Уже первый вопрос, который задаёт судья каскада, отдаёт победу вашему обычному объявлению над обычным объявлением браузера, каким бы слабым ни был ваш селектор. Даже * { margin: 0; } снимает отступы и с заголовка, и с абзаца. Воевать с листом браузера не нужно — его достаточно перекрыть.

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

Остаётся вопрос: откуда 14px? В настройках браузеров по умолчанию стоит шрифт 16px, и на обычной странице заголовок получил бы 32px, а не 28px. Значит, шрифт кто-то поменял ещё до нас.

Страница примера — не голый документ: учебник собирает её сам и может положить туда свои правила. Проверить это позволяет rules(): она перечисляет листы стилей документа — по строке на лист, с номером и селекторами его правил, без значений. Лист браузера в этот список не попадает: он живёт внутри браузера, а не в документе. В разметке примера нет ни одного <style> — в <head> только кодировка и заголовок. Прогноз: сколько листов окажется в документе и что в них будет?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Одна строка, и в ней два правила: для body и для *, то есть для любого элемента. Это сброс, который песочница учебника кладёт в каждую страницу примера. Значения прибор не печатает, поэтому вот они словами. У body — margin: 0, padding: 10px и шрифт 14px с высотой строки 1.5: отсюда 28px у заголовка. Браузер даёт body отступ 8px со всех сторон, и сброс заменил его своим. Правило для * ставит всем элементам box-sizing: border-box: ширина элемента включает его внутренний отступ и рамку.

Если в разметке примера есть свой <style>, разбор покажет его следующей строкой — например, лист 2: [hidden] | .row: свой лист встаёт в <head> после сброса. Так всегда можно убедиться, что на странице нет чужих правил, о которых вы не знаете, — приём полезный и за пределами учебника, когда чужой стиль внезапно оказывается сильнее вашего.

Что box-sizing меняет в размерах и чем em отличается от rem, глава разбирает дальше — ширина коробки и единицы em и rem. Для примеров пролога важно одно: сброс песочницы стоит всегда, и все замеры сделаны поверх него.

Отступ 0.67em у заголовка придумали не вчера. У стандарта CSS 2.1, принятого W3C в 2011 году, было приложение — образец листа стилей для HTML. Составили его, как сказано в нём самом, по исследованию того, как браузеры на практике рисуют страницы, и там уже стоит h1 { font-size: 2em; margin: .67em 0 }. Но образец был только советом, и в мелочах браузеры расходились.

В 2010-х эти расхождения выравнивал файл normalize.css. Его подключали первым, до своих стилей, и у каждого правила в нём есть подпись: что оно чинит и в каких браузерах. Три правила из последнего выпуска, 8.0.1 от 9 ноября 2018 года: main { display: block; } — чтобы <main> одинаково рисовался в Internet Explorer; details { display: block; } — правильный display в Edge, Internet Explorer и Firefox; summary { display: list-item; } — правильный display во всех браузерах. Значение list-item — блок с маркером, как у пункта списка. На странице ниже этих правил нет. Прогноз: какой display окажется у трёх элементов?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
block, block, list-item — ровно то, что дописывал normalize.css. Браузеры с тех пор сами записали эти значения в свои листы, и правила стали повторять умолчание. Та же судьба у многих правил файла: из 32 подписей 13 называют Internet Explorer, а поддержку Internet Explorer 11 Microsoft прекратила 15 июня 2022 года. Отживает даже правило для h1 внутри section и article, подписанное «для Chrome, Firefox и Safari»: в статье об ориентирах Chrome и Firefox нарисовали заголовок внутри раздела того же размера, что и снаружи.

Несколько забот normalize.css живы и сегодня, например шрифт полей формы — о нём ниже. Остальное заменяют несколько строк своего сброса, и у каждой должна быть причина, которую можно измерить.

Атрибут hidden прячет элемент — так кажется. На самом деле прячет строка из листа браузера: [hidden] { display: none; }. Значение none убирает элемент со страницы совсем: ни места, ни текста. Но это обычное правило CSS, и в споре с вашими правилами оно подчиняется тому же судье — без всякого уважения к тому, что атрибут поставлен в разметке.

В примере блок с классом row: правило даёт ему display: flex — так раскладывают в ряд пункты меню и кнопки панели. Рядом абзац без своих правил. hidden стоит у обоих. Прогноз: какой display окажется у блока и у абзаца?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Абзац спрятан: none пришёл из листа браузера. А блок на экране — flex, хотя hidden у него тоже есть. Спор решился на первом вопросе судьи: ваше обычное объявление сильнее обычного объявления браузера, и до специфичности дело не дошло. Коварство в том, что правило .row могли написать давно, и о hidden в нём никто не думал, а сломалось всё там, где элемент решили спрятать.

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

Лечится одной строкой. Тот же первый вопрос судьи отделяет важные объявления от обычных, поэтому display: none !important выиграет у любого обычного display, как бы сильно ни был написан селектор и где бы ни стояло правило. Обычно !important — признак поражения, но здесь редкий случай, когда он уместен: вы чините поведение атрибута, а не спорите со стилями. Прогноз: какой display будет у блока теперь?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
none: блок спрятан. Это редкий случай, когда !important в сбросе оправдан. hidden ставят ровно для того, чтобы элемент исчез, и никакой display в стилях не должен этому мешать. Чтобы элемент показать, снимают атрибут, а не перебивают правило.

Элемент с hidden всё равно виден? Первым делом ищите у него display в своих стилях: скорее всего, какой-то класс перекрыл правило браузера.

В части II тот же атрибут встретится со стороны JavaScript: у элемента есть свойство hidden, и оно равно true, пока атрибут стоит, — атрибуты и свойства.

Вторая ловушка уже встречалась в разборе наследования: кнопка не взяла размер шрифта у родителя. Причина та же — лист браузера. Кнопкам <button>, полям ввода <input>, спискам выбора <select> и многострочным полям <textarea> он назначает свой шрифт, а объявление для самого элемента, даже браузерное, сильнее наследства.

Два одинаковых блока с размером шрифта 18px, в каждом текст и кнопка. Во втором кнопке сказано font: inherit. Прогноз: в каком блоке размер шрифта у текста и кнопки совпадёт?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
В первом блоке размеры разные: кнопка осталась со шрифтом браузера. Во втором одинаковые. font — сокращённое свойство, и inherit в нём просит у родителя всё сразу: семейство шрифта, размер, насыщенность, высоту строки. Одно font-size: inherit сравняло бы только размер, а семейство у кнопки осталось бы браузерным. Пикселей пример не печатает, как и в разборе наследования: размер шрифта кнопки в Chrome и Firefox чуть различается.

В сбросе это правило пишут сразу для всех четырёх: button, input, select и textarea. Тогда текст в полях выглядит так же, как текст страницы вокруг, и меняется вместе с ним — в том числе когда человек увеличил шрифт в настройках браузера, а форма осталась прежней и перестала читаться.

Сброс ставят в самое начало стилей в расчёте на то, что всё написанное ниже его перекроет. Но порядок в коде — последний вопрос судьи, и до него дело доходит, только если специфичность равна. Если селектор сброса сильнее, он выиграет у правил компонентов, где бы они ни стояли. В примере два сброса отступа для списков с классом, и после каждого — правило компонента. Первый сброс написан как есть, второй завёрнут в :where(). Посчитайте специфичность и предскажите оба отступа.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
ul[class] весит (0, 1, 1) — атрибут и тег — и бьёт .menu с его (0, 1, 0), хотя стоит выше: меню осталось без отступа. :where(ol[class]) весит ноль, и .steps побеждает. Отсюда правило: сброс пишут на :where(). Он должен задавать умолчания, а не спорить с вашими классами. Исключение одно — [hidden]: там спор нужно выиграть, и выигрывает его !important. А * заворачивать незачем: он и так весит ноль.

Соберём сброс целиком. Строк «на всякий случай» в нём нет: каждая отвечает на замер, который мы уже сделали, или записывает осознанное решение.

Строка сбросаНа что отвечает
[hidden] { display: none !important; }любой display в ваших стилях возвращал на экран элементы с hidden
:where(button, input, select, textarea) { font: inherit; }поля формы не брали шрифт страницы
:where(ul[class], ol[class]) { padding-left: 0; }списки-меню получали 40px слева; у обычных списков в тексте, без класса, отступ остаётся
* { box-sizing: border-box; }не починка, а решение о том, что считать шириной элемента; в песочнице оно уже принято

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

Откуда пришло значение, видно и на живой странице: в инструментах разработчика Chrome блок Styles подписывает правила браузера отдельно, в английском интерфейсе — словами «user agent stylesheet». Сама панель разобрана в части II — инспектор элементов.

  • У браузера свой лист стилей, и он действует на каждой странице: у h1 размер 2em и отступы 0.67em, у списка 40px слева, у абзаца отступы 1em. Любое ваше обычное объявление сильнее обычного объявления браузера.
  • В font-size единица em считается от шрифта родителя, в отступах — от шрифта самого элемента: 2 × 14px = 28px, а 0.67 × 28px = 18.76px.
  • У страницы примера свой сброс: rules() находит лист с правилами для body и *. В нём у body шрифт 14px, margin: 0 и padding: 10px, а у всех элементов box-sizing: border-box.
  • Атрибут hidden прячет только правилом браузера [hidden] { display: none; }, и любой ваш display его перебивает. В сброс ставят [hidden] { display: none !important; }.
  • Кнопки и поля формы не наследуют шрифт страницы: браузер назначает им свой. font: inherit возвращает им шрифт родителя целиком.
  • Сброс пишут на :where(), чтобы он задавал умолчания и уступал любому классу. Каждая его строка — ответ на измеренную проблему.

1/4На странице нет ни одного правила CSS. Откуда у <h1> отступ сверху?

2/4У элемента атрибут hidden и класс panel, а в стилях есть только .panel { display: block; }. Виден ли элемент?

3/4Правило :where(ul[class]) { padding-left: 0; } стоит в коде ниже, чем .menu { padding-left: 24px; }. Какой отступ слева у <ul class="menu">?

4/4Чем font: inherit у кнопки отличается от font-size: inherit?

Панель, которая не прячется

У первой панели и у абзаца с акцией стоит hidden, но прибор показывает, что оба на экране. Допишите одно правило в начало <style>, чтобы hidden прятал всегда, а видимая панель осталась flex. Существующие правила не меняйте.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Прибор напечатает .toolbar, #promo → display: none, flex, none. Без !important правило не спасёт даже в конце <style>. У .toolbar и [hidden] специфичность одинаковая, (0, 1, 0), и нижнее выиграет порядком — панель спрячется. Но #promo весит (1, 0, 0), и акция останется на экране: none, flex, block. Важность судья проверяет раньше специфичности и порядка, поэтому правилу с !important всё равно, где стоять и с какими селекторами спорить.

Сброс перебил маленькое поле

Сброс даёт полям формы шрифт страницы, а класс small уменьшает поле до 12px. У textarea так и вышло, а текстовое поле осталось 18px. Объясните почему и перепишите только строку сброса, чтобы оба поля стали 12px. Класс small и разметку не трогайте.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Посчитайте вес сброса. textarea — один тег, (0, 0, 1), и .small с его (0, 1, 0) побеждает. input[type="text"] — тег и атрибут, (0, 1, 1): сброс сильнее класса, хотя стоит выше. Заверните список в :where():

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Прибор напечатает .small → font-size: 12px, 12px. Можно сразу взять строку из сброса статьи, :where(button, input, select, textarea): шрифт страницы получат все поля, а класс по-прежнему их поправит.

Посчитайте до запуска

Своего <style> на странице нет, только атрибут style у блока: в нём размер шрифта 20px, а внутри лежат заголовок и список. Не запуская пример, посчитайте три числа, которые напечатает прибор, и только потом запустите.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

h1 → font-size: 40px: в font-size единица em — размер шрифта родителя, 2 × 20px. h1 → margin-top: 26.8px: в отступе em — уже размер шрифта самого заголовка, 0.67 × 40px. ul → padding-left: 40px: отступ списка записан в пикселях и от шрифта не зависит. Два числа 40 совпали случайно: поставьте блоку 15px, и заголовок станет 30px, а отступ списка останется 40px.