Отличие между успешным использованием CSS и мучительными попытками справиться с ним, зачастую зависит от мелких деталей. На самом деле, в CSS очень много нюансов. Одна из наиболее часто встречающихся областей, где я часто замечаю такую борьбу — это стилизация макетов. Лично мне нравится изучать CSS-паттерны. Я заметил, что склонен использовать небольшое их количество для решения большинства проблем с макетом. Эта статья про те CSS-паттерны, которые я использую для преодоления проблем в вёрстке. Ситуации будут рассматриваться независимо от используемой CSS-методологии: будь то SMACSS, BEM или даже горячая тема CSS-in-JS, потому что все они сфокусированы на самих CSS-свойствах, а не на архитектуре, организации или стратегии.
Ради забавы, начнем с теста
Мы будем использовать разработанную мною платформу, которая называется Questionable.io и я использовал её для создания теста, к которому перейдем ниже. Не беспокойтесь, никакие персональные данные не собираются, результаты анонимны и это абсолютно бесплатно.
Цель тестирования в том, чтобы увидеть можете ли вы распознать специфичное поведение CSS и некоторые проблемы без предварительного ознакомления с материалом статьи. Я не собирался делать тестирование сложным, но нюансы CSS-стилизации могут быть довольно сложными. Помните, это тестирование просто ради забавы. Результаты теста не демонстрируют вашу крутость, но надеюсь, всё же будут полезными.
Тест состоит из 10 вопросов и должен занять не более 10 минут
От переводчика:
в конце статьи на Хабре добавлен опрос про количество набранных очков в тесте.
ПРОЙТИ ТЕСТ
Примечание: если не хотите тратить время, вот ссылка на вопросы с правильными ответами.
Прошли тест? Отлично! Давайте пройдемся по вопросам один за другим, чтобы получить лучшее представление о паттернах стилизации, которые затрагиваются в тесте.
Вопрос 1: Блочная модель
Изучение блочной модели должно быть приоритетным в списке любого FrontEnd-разработчика. Может статья «The CSS Box Model» немного и старовата, не стоит недооценивать её ценность и актуальность для современного CSS. Блочная модель является основой почти для каждого вопроса, относящегося к CSS-макетам.
Этот конкретный вопрос проверяет, как узнать ширину с учетом принципов блочной модели. Блок имеет явно заданную ширину через свойство width: 100px
, но оказывается, что правила блочной модели по умолчанию, применяют свойства width
к слою «content» данного блока. Итоговая ширина (насколько широким блок отрисовывается на странице) является суммой слоёв content, padding и border. Именно по этой причине ответ — 112px.
.box {
width: 100px; /* Take this */
height: 50px;
padding: 5px; /* Plus this x2 for left and right */
border: 1px solid red; /* Plus this x2 for left and right */
background-color: red;
/* = 112px of computed width */
}
Если вы сталкивались с ситуацией, в которой последняя колонка или вкладка в интерфейсе переносится вниз на следующую строку, хотя вы указали для всех width: 20%;
и были уверены, что они поместятся в 100% родительского элемента, вероятно, это становилось проблемой. Пять колонок шириной по 20% помещаются в 100%, но если у них дополнительно задан padding и/или border, это увеличит ширину каждой, оставив недостаточно места в текущей строке для последней колонки.
Когда был представлен CSS3, появился новый инструмент под названием box-sizing
. Он позволяет нам управлять тем, к какому слою блочной модели мы хотим применять свойство width
. Например, мы можем указать box-sizing: border-box
. Это означает, что мы хотим, чтобы любые правила width применялись к внешнему слою «border» вместо слоя «content». В вопросе из теста, если бы было применено свойство box-sizing: border-box
, итоговая ширина блока составила 100px.
Для некоторых из вас это уже известный факт, но всё же будет неплохим напоминанием как для профессионалов, так и для новичков.
Существует целый ряд статей про блочную модель и про то, как использовать свойство box-sizing в качестве сброса, чтобы применить его сразу ко всему проекту. Box Sizing и Inheriting box-sizing Probably Slightly Better Best-Practice — две хорошие статьи с сайта CSS-Tricks, с которыми можно ознакомиться.
Вопрос 2: Границы элемента
Второй тестовый вопрос отчасти можно рассматривать как продолжение первого. Помните, одно дело прочитать: «Блочная модель имеет слои (content, padding, border) и все они влияют на итоговую ширину и высоту элемента», другое дело — уметь распознать проблемы блочной модели в реальной ситуации. Эта конкретная проблема является чем-то вроде классики среди тех, кто уже некоторое время работает с CSS. Она вытекает из того факта, что рамки займут определенное дополнительное пространство и оттолкнут окружающие элементы, поскольку являются частью блочной модели. Добавление рамок во время изменения состояния элемента, такого как :hover
, будет значить, что блоки станут больше и тогда оттолкнут идущие следом блоки вниз. Это может раздражать:
Из всех возможных решений, указанных в тестовом вопросе, добавление border: 2px solid transparent
к изначальному состоянию (без наведения курсора) является единственным возможным решением проблемы. box-sizing
не решает эту проблему, потому что мы явно не задаём высоту. Если бы мы сделали это, рамка стала частью общей высоты элемента и сдвиг не происходил, но это не наш случай.
Существуют другие решения, которые мы не упомянули в вариантах ответа. Один из них — это добавление псевдо-рамки с помощью свойства box-shadow
или использование outline
вместо border
. Каждый из них не будет приводить к смещению, так как не является слоем блочной модели. Вот другая статья на CSS-Tricks, где можно больше почитать про подобные решения.
Примечание:
Помните, что outline
не поддерживает border-radius
Вопрос 3: Позиционирование — absolute против fixed
Помимо понимания того, когда использовать каждый из типов и как они отличаются в визуальном представлении, также очень важно знать правила того, как каждый из методов позиционирования привязывается к родительскому элементу с помощью свойств top, right, bottom или left.
В первую очередь, давайте рассмотрим содержащий блок. Краткое определение заключается в том, что содержащий блок чаще всего — это родитель любого рассматриваемого элемента. Тем не менее, правила для содержащего блока разные у абсолютно и фиксировано позиционированных элементов.
1) Для абсолютных элементов. Содержащий блок — это ближайший предок, у которого позиционирование не static
. Например, когда элемент абсолютно позиционирован, и содержит свойства top
, right
, bottom
или left
, они будет позиционироваться относительно любого родителя, который имеет позиционирование absolute
, relative
, fixed
или sticky
.
2) Для фиксированных элементов. Содержащий блок — это область просмотра, несмотря на наличие любых родительских элементов с позиционированием, отличным от static
. Кроме того, поведение прокрутки отличается от абсолютно позиционированных элементов. Фиксированные элементы остаются «зафиксированными» в области просмотра, отсюда и название.
Многие разработчики думают, что абсолютно позиционированные элементы ищут лишь ближайший родительский элемент с относительным позиционированием position: relative
. Это заблуждение стало распространённым просто потому, что position: relative
чаще всего указывают в паре с position: absolute
, чтобы сделать содержащий блок. Причина, по которой это часто используется в том, что относительное позиционирование родительского блока оставляет его в потоке, что зачастую является предпочтительным. Встречаются ситуации, когда родитель абсолютно позиционированного элемента сам является абсолютно позиционированным. Это совершенно нормальное явление. Если все родители статичные, тогда абсолютно позиционированный элемент будет прикрепляться к области просмотра — но так, чтобы прокручиваться вместе с областью просмотра.
Существует малоизвестное дополнение к правилам, указанным выше: в ситуациях, когда родительский элемент имеет свойство transform
(помимо прочих) со значением, отличным от none
, он становится содержащим блоком для абсолютно и фиксированно позиционированных элементов. Подтверждение этому можно увидеть в CodePen, где элемент с текстом «Notice!» — это фиксированный элемент, а родитель имеет свойство transform, но только когда на него наведен курсор мыши (в состоянии :hover
)
Вопрос 4: Схлопывание margin родительского и дочерних элементов
Это одна из тех CSS мелочей, которая может доставить немало проблем, если вы не знаете, как она работает. Существует концепция, называемая схлопыванием margins и много людей знакомы с проявлением этого, называемым Схлопыванием margins смежных сестринских элементов. Тем не менее, существует и другая форма, называемая Схлопыванием margins родительского и первого/последнего дочернего элемента, которая менее известна. Ниже приведена демонстрация обоих случаев:
Каждый тег параграфа имеет верхний и нижний margin, равный 1em, что стилизуется браузером. Пока это самая легкая часть ситуации. Но почему промежуток между параграфами не 2em (сумма верхнего и нижнего)? Это называется схлопыванием margins смежных сестринских элементов. Margins перекрываются таким образом, что больший из двух margin будет составлять размер разрыва, таким образом, в данном случае разрыв будет равен 1em.
Однако происходит еще кое-что странное. Вы заметили, что верхний margin первого параграфа не создает промежуток между ним и голубым контейнером div? Вместо разрыва, он будто вкладывает margin в родительский div, как если бы div имел верхний margin. Это называется Схлопывание margins родительского и первого/последнего дочерних элементов. Этот тип схлопывания margins не будет происходить при определенных обстоятельствах, если родительскому элементу свойственно следующее:
- Присутствует верхний/нижний padding больший нуля
- Присутствует верхний/нижний border больший нуля
- Задан Блочный контекст форматирования, который может быть создан с помощью
overflow: hidden
илиoverflow: auto
- Задано свойство display: flow-root (плохая поддержка браузерами)
Когда я с удовольствием объясняю людям эту небольшую CSS-деталь и решаю это с помощью padding или border, ответ почти всегда: «А что насчет padding или border, равных 0?». Ну, это не работает, потому что значение должно быть целым положительным числом.
В предыдущем примере, padding размером в 1px позволяет нам переключиться между использованием схлопывание и предотвращением схлопывания margins родителя и дочернего элемента. Промежуток, который отображается между первым/последним параграфом и родительским элементом, это padding размером 1px, но теперь margin считается внутренней частью контейнера, поскольку слой padding создаёт барьер, предотвращающий схлопывание margins.
Что касается вопроса, я уверен, что вы можете увидеть, в чем проблема в этом интерфейсе:
У первого элемента с классом .comment
(без класса .moderator
) происходит схлопывание margins. Даже без просмотра кода мы можем увидеть, что у элемента с классом .moderator
есть рамка, а у элемента без этого класса нет. В этом вопросе фактически было три ответа, которые считались правильными.Каждый из них на самом деле, уже применен в исходнике CodePen, они просто закомментированы.
Одна из причин, по которой этот тип схлопывания margins не так широко известен, как другие, заключается в том, что имеется множество ситуаций, при которых мы можем случайно избежать этого. Flexbox и Grid-элементы создают блочный контекст форматирования, поэтому при их использовании мы не сталкиваемся с этим типом схлопывания margins. Если бы интерфейс наших комментариев был реальным проектом, велики шансы, что со всех четырех сторон у нас были заданы paddings, чтобы оставить пространство вокруг, а это, в свою очередь, исправило для нас схлопывание margins.
Вопрос 5: Процент от чего?
Когда используются единицы измерения в процентах, проценты основываются на ширине или высоте содержащего блока (обычно это родительский элемент). Как мы уже говорили ранее, элемент со свойством transform
станет содержащим блоком, поэтому когда элемент использует transform, единицы измерения в процентах (только для transform
) основываются
на собственном размере элемента, а не на размере родителя.
В примере ниже мы можем видеть, что 50% принимают два разных значения в зависимости от контекста. Первый красный блок имеет свойство margin-left: 50%
, а второй красный блок использует transform: translateX(50%)
.
https://codepen.io/bradwestfall/pen/Kbzyrq
Вопрос 6: Блочная модель наносит новый удар...
Только вы подумали, что мы закончили говорить о блочной модели…
https://codepen.io/bradwestfall/pen/maOJmO
Проблема проявляется из-за того, что мы используем width: 100%
для футера и одновременно с этим добавляем padding. Ширина контейнера равняется 500px, что значит, что слой content футера (будучи 100%) также равняется 500px еще до того, как к этому размеру добавятся paddings.
Проблема может быть исправлена одной из двух распространенных техник:
- Использовать свойство box-sizing для футера напрямую или через сброс, который упоминался раньше
- Удалить у элемента свойство
width
и вместо него применить свойстваleft: 0
иright: 0
. Это хороший пример одновременного использования свойствleft
иright
. Такой подход позволит избежать проблем блочной модели, потому что свойствоwidth
будет использовать своё значение по умолчаниюauto
, чтобы заполнить любое доступное пространство между padding и borders, когда установленыleft: 0
иright:0
.
Примечание: одним из вариантов было удаление padding у футера. Технически это исправило бы проблему, потому что слой content был бы 100% и не имел padding или border, чтобы расшириться за пределы ширины контейнера. Но я думаю, это решение является неправильным подходом, потому что мы не должны менять наш интерфейс, чтобы приспособиться к проблемам блочной модели, которые и без этого легко исправляются.
Реальность для меня заключается в том, что у меня всегда выставлено свойство box-sizing: border-box
, как часть моего общего сброса стилей. Если вы поступаете так же, скорее всего редко сталкиваетесь с этой проблемой. Но мне все равно нравится приём с одновременной установкой свойств left:0
и right:0
, потому что, как показывается время, он более надежен (во всяком случае, судя по моему опыту), чем решение проблем блочной модели, возникающих из-за ширины 100%.
Вопрос 7: Центрирование абсолютных и фиксированных элементов
Теперь мы действительно начинаем объединять весь материал, описанный выше, с центрированием абсолютных и фиксированных элементов:
Поскольку мы уже охватили большую часть материала этого тестового вопроса, я просто обозначу, что горизонтальное и вертикальное центрирование может быть выполнено «олдскульным» методом через отрицательные margins, или более новым с помощью свойства transform. Также представляю вашему вниманию отличное CSS-Tricks руководство по центрированию элементов.
Примечание: некоторое время назад утверждали, что если мы знаем ширину и высоту блока, должны использовать отрицательные margins, потому что они работают более стабильно, чем новое свойство трансформации. В данный момент трансформация работает стабильно и я почти всегда использую именно это свойство, если только мне не нужно избежать преобразования блока в содержащий.
Вопрос 8: Центрирование элементов в нормальном потоке
Flexbox принес нам много удивительных инструментов для решения сложных задач с макетом. Перед его релизом сообщали, что вертикальное центрирование было одной из наиболее сложных функций, которые реализовывались в CSS. С приходом Flexbox вертикальное центрирование стало чем-то обыденным:
.parent { display: flex; }
.child { margin: auto; }
https://codepen.io/bradwestfall/pen/GPNmbM
Примечание: заметьте, что у flex-элементов margin: auto применяется к верху, низу, правой и левой стороне, чтобы центрировать элемент вертикально и горизонтально. Раньше вертикальное центрирование не работало для блочных элементов, поэтому достаточно распространена запись margin: 0 auto;
Вопрос 9: Расчеты с разными единицами измерения
Использование функции calc() прекрасно, когда нужно работать с двумя единицами измерения, которые мы не можем сложить самостоятельно или когда нам нужно сделать дроби более простыми для восприятия. Этот тестовый вопрос предлагает нам выяснить, каким будет результат выражения calc(100% + 1em)
, отталкиваясь от того факта, что ширина элемента div равна 100px. Это могло немного запутать, потому что на самом деле, не имеет значения, что ширина элемента div равняется 100px. Проценты всегда отталкиваются от ширины родителя, поэтому правильным ответом был: «100% содержащего (родительского) блока плюс 1em».
Существует несколько ситуаций, в которых я регулярно использую calc()
. Во-первых, всякий раз, когда я хочу сместить что-то на 100%, но также добавить фиксированное количество дополнительного пространства. Выпадающие меню могут быть хорошим примером этого:
Особенность здесь в том, что мы хотим сделать выпадающее меню, которое может быть использовано вместе с вызывающими его элементами разного размера (в этом случае, два разных размера кнопок). Мы не знаем, какая будет высота элемента, вызывающего это меню, но мы знаем, что top: 100%
разместит верхний край выпадающего меню у нижнего края вызывающей кнопки. Если каждое меню должно быть у нижнего края соответствующей кнопки, плюс 0.5em, этого можно добиться с помощью calc(100% + 0.5em)
. Конечно, мы также могли использовать top: 110%
, но эти дополнительные 10% зависели бы от высоты вызывающей кнопки и контейнера.
Вопрос 10: Отрицательные margins
В отличие от положительных margins, которые отталкивают сестринские элементы, отрицательные margins притягивают их ближе друг к другу без смещения сестринских элементов. Этот финальный вопрос теста предлагает два решения, оба из которых технически приводят к устранению двойной рамки в нашей группе кнопок, но я настоятельно предпочитаю технику отрицательного margin, потому что удаление рамок сделало бы более сложным выполнение определенных приёмов, таких как эффект наведения мыши.
Получившийся эффект представляет собой «общую границу», которая отображается между кнопками. На самом деле, кнопки не могут иметь общую границу, поэтому нам нужно использовать приём с отрицательным margin, чтобы одна рамка перекрывала другую. Потом я использую z-index
, чтобы устанавливать, какую рамку я хочу поместить выше другой, основываясь на состоянии наведения мыши. Обратите внимание, что z-index
полезен здесь даже без использования абсолютного позиционирования, но пришлось задать position: relative
. Если бы я использовал технику удаления левой рамки у второй кнопки, этот эффект было бы гораздо сложнее реализовать.
Всё это имеет смысл
Я хочу показать вам еще одно последнее демо, которое использует много приёмов, которые мы обсуждали до этого. Задача — создать плитки, которые расширяются до левого и правого края контейнера с учетом отступов. Под плитками я подразумеваю возможность иметь список блоков, которые переносятся на следующую строку, когда места по ширине не хватает. Наведите на плитки, чтобы увидеть результат:
https://codepen.io/bradwestfall/pen/JwbVRp
Препятствием при выполнении этой задачи являются отступы. Без отступов было бы просто получить плитки, прилегающие к левому и правому краю контейнера. Проблема в том, что отступы будут созданы с помощью margins, а когда мы добавляем margins со всех сторон плитки, мы создаем две проблемы:
- Наличие трёх плиток с шириной
33.33%
в совокупности с margin не смогут поместиться в строке. Хотяbox-sizing
позволяет нам иметь padding и borders у элемента.tile
и умещаться в33.33%
, это не поможет нам в случае с margins — а это значит, что рассчитываемая ширина трёх плиток будет больше 100%, что вынудит последнюю плитку перенестись на следующую строку. - Крайняя левая и правая плитки больше не будут прилегать к краю контейнера.
Первая проблема может быть решена с помощью calc((100% / 3) - 1em)
. Что значит 33.33% минус левый и правый margins каждой плитки. Схлопывание смежных сестринских элементов здесь происходить не будет, так как оно возможно только у верхнего и нижнего margin. В результате горизонтальное расстояние между двумя плитками — это сумма двух margins (1em). В этом случае схлопывание также не применится к верхним и нижним margins, потому что первая и последняя плитка технически не являются смежными, даже если визуально одна находится под другой.
С помощью приёма с calc()
, три плитки могут поместиться в строке, но они все еще не прикасаются к краям контейнера. Для этого мы можем использовать отрицательные margins в размере, равном левому и правому margin каждой плитки. Зеленая точечная линия в примере — это контейнер, где мы применим отрицательные margins, чтобы нарисовать плитки, соответствующих краю окружающего содержимого. Мы можем видеть, как они прилегают к области padding родительского элемента. Это нормально, потому что отрицательные margins не отталкивают окружающие соседние элементы.
В итоге мы получаем плитки, которые имеют достаточные промежутки, которые расширяются от края до края так, что они выравниваются с соседними тегами абзаца вне плиток.
Существует много способов сделать плитки (и обычно они имеют свои преимущества и недостатки). Например, существует довольно элегантное решение с использованием CSS Grid, над которым рассуждал Хейдон Пикеринг. Данное решение реализуется с использованием техники, которая имитирует запросы к контейнерам (но с помощью магии Grid). В конечном счете, его решение с использованием Grid лучше, чем flexbox, которое я продемонстрировал, но имеет худшую поддержку браузерами.
Резюме
В начале я заявил, что склонен искать шаблоны при решении проблем. Эта статья не столько о продемонстрированных выше сценариях; она больше о наборе инструментов, которые можно использовать для решения этих и многих других проблем вёрстки, с которыми мы все можем столкнуться. Я надеюсь, что эти инструменты помогут вам.
Кстати, есть ряд отличных ресурсов, которые тщательно разбирают тему блочной модели. В первую очередь, статьи Рэйчел Эндрю и Джен Симмонс, с которыми определенно стоит ознакомиться.
- Box Alignment Cheatsheet — отличный материал с визуальными элементами, которые подчеркивают различные свойства, которые влияют на то, как элементы выравниваются либо сами по себе, либо относительно других элементов
- Jen Simmons Labs — множество полезных публикаций, демо и экспериментов с использованием современных методов разметки.
Автор: Зварич Рома