Защитный CSS — коллекция сниппетов

Источник: «Defensive CSS»
Часто хочется, чтобы был способ избежать возникновения определённой проблемы или поведения CSS. Вы знаете, что контент динамический, и на веб-странице всё может измениться, что увеличивает вероятность возникновения проблемы с CSS или странного поведения.

Защитный CSS — это коллекция сниппетов помогающая вам в написании CSS, который будет защищён. Другими словами, в будущем у вас будет меньше проблем. Если вы следите за моим блогом, вы могли прочитать статью «The just in case mindset». Эта статья основывается на ней и будет текущим списком сниппетов.

Перенос Flexbox

CSS Flexbox — одна из самых полезных функций CSS макета. Заманчиво, добавить display: flex к элементу и дочерние элементы выстроятся один за другим.

Дело в том, что когда места недостаточно, по умолчанию, дочерние элементы не переносятся на новую строку. Нам нужно изменить это поведение с помощью flex-wrap: wrap.

Вот типичный пример. У нас есть группа опций, которые должны отображаться рядом друг с другом.

.options-list {
display: flex;
}
display: flex;

Когда места становится меньше, появляется горизонтальная прокрутка. Этого следовало ожидать, и на самом деле это не "проблема".

Когда места становится меньше, появляется горизонтальная прокрутка

Обратите внимание, как предметы по-прежнему находятся радом друг за другом. Что бы исправить это, нам нужно разрешить перенос.

.options-list {
display: flex;
flex-wrap: wrap;
}
Разрешите перенос, если вам не нужна горизонтальная прокрутка

Общее практическое правило при использовании flexbox — разрешить перенос, если вам не нужна горизонтальная прокрутка. Это другое дело, но попробуйте использовать flex-wrap, чтобы избежать неожиданного поведения макета (в нашем случае горизонтальной прокрутки).

Отступ

Нам, разработчикам, необходимо учитывать разную длину контента. Это означает, что к компоненту нужно добавить отступ, даже если он кажется ненужным.

Нужно добавить отступ, даже если он кажется ненужным.

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

Что произойдёт, если заголовок станет длиннее

Заметили, что текст расположен слишком близко к кнопке? Возможно, вы думаете о многострочном переносе, но я вернусь к этому в другом разделе. А пока давайте сосредоточимся на отступе.

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

.section__title {
margin-right: 1rem;
}
Если в заголовке есть отступы и усечение текста, мы не увидим такой проблемы.

Длинный контент

Учёт длинного контента важен при создании макета. Как вы могли видеть в предыдущем случае, заголовок раздела обрезается, если он слишком длинный. Это необязательно, но для некоторых пользовательских интерфейсов важно учитывать.

Для меня это защитный CSS подход. Приятно решить «проблему» до того, как она действительно случится.

Вот список имён людей, и на данный момент он выглядит идеально.

Вот список имён людей, и на данный момент он выглядит идеально

Однако, поскольку это контент, созданный пользователями, мы должны быть осторожны с тем, как защитить макет на случай, если кажется что-то окажется слишком длинным.

Смотрите, следующий рисунок:

В таких макетах важна согласованность.

В таких макетах важна согласованность. Для этого мы можем просто обрезать имя, используя text-overflow и его друзей.

.username {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Мы можем просто обрезать имя, используя text-overflow

Если вы хотите отточить свои навыки обработки длинного контента в CSS, я написал подробную статью на эту тему.

Предотвращение растягивания или сжатия изображения

Когда у нас нет контроля над соотношением сторон изображения на веб странице, лучше подумать заранее и предложить решение когда пользователь загружает изображение, которое не соответствует нужному соотношению сторон.

В следующем примере у нас компонент карточки с фотографией. Выглядит хорошо.

Выглядит хорошо.

Когда пользователь загружает изображение другого размера, оно будет растянуто. Это нехорошо. Посмотрите, как растягивается изображение!

Это нехорошо. Посмотрите, как растягивается изображение!

Самый простой способ исправить это — использовать CSS свойство object-fit.

.card__thumb {
object-fit: cover;
}
Использовать CSS свойство object-fit

На уровне проекта, я предпочитаю добавлять object-fit ко всем изображениям, чтобы избежать неожиданных результатов.

img {
object-fit: cover;
}

Узнать больше об object-fit вы можете из статьи «A Deep Dive Into object-fit And background-size In CSS» на Smash Magazine

Блокировка цепочки прокрутки

Вы когда-нибудь открывали модальное окно и начинали прокрутку, а затем когда прокручивали до конца и продолжали прокрутку, контент под модальным окном (элемент body) тоже прокручивался? Это называется цепочка прокрутки.

Было придумано несколько хаков, чтобы предотвратить это. Но, теперь мы можем делать это с помощью CSS свойства overscroll-behavior.

На следующем рисунке вы увидите поведение цепочки прокрутки по умолчанию.

Поведение цепочки прокрутки по умолчанию.

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

.modal__content {
overscroll-behavior-y: contain;
overflow-y: auto;
}
overscroll-behavior-y: contain

Если вы хотите узнать об этом свойстве по больше, я написал более подробную статью.

Резервные значения CSS переменных

CSS переменные всё чаще используются в веб-дизайне. Есть метод, есть метод позволяющий использовать их, если значение CSS переменной по какой-то причине оказалось пустым.

Это особенно полезно при передаче значения переменной через JavaScript. Вот пример:

.message__bubble {
max-width: calc(100% - var(--actions-width));
}

Переменная --actions-width используется в функции calc(), а её значение поступает из JavaScript. Предположим, что JavaScript по какой-то причине не работает, что будет? max-width вычисляется как none.

Мы можем избежать этого заранее и добавив резервное значение в var().

.message__bubble {
max-width: calc(100% - var(--actions-width, 70px));
}

Таким образом, если переменная не определена, будет использовано резервное значение (70px). Этот подход можно использовать, если существует вероятность сбоя переменной (например, получаемой из JavaScript). В противном случае в этом нет необходимости.

Использование фиксированной ширины или высоты

Одна из распространённых вещей, нарушающих макет, — это использование фиксированной ширины или высоты с элементом, который имеет содержимое разной длинны.

Фиксированная высота

Я часто вижу секцию hero с фиксированной высотой и содержимым, превышающим эту высоту, что приводит к нарушению макета. Не знаете, как это выглядит? Вот так:

.hero {
height: 350px;
}
Фиксированная высота

Что бы контент не выходил за пределы секции hero, нужно использовать min-height вместо height.

.hero {
min-height: 350px;
}
Нужно использовать min-height

Таким образом, если контент станет больше, макет не сломается.

Фиксированная ширина

Вы когда-нибудь видели кнопку, название которой расположено слишком близко к левому и правому краям? Это связано с использованием фиксированной ширины.

.button {
width: 100px;
}

Если подпись кнопки длиннее 100 пикселей, она будет слишком близко к краям. Если она будет слишком длинной, текст будет выползать за края. Это нехорошо!

Это связано с использованием фиксированной ширины.

Чтобы исправить это, мы можем просто заменить width на min-width.

.button {
min-width: 100px;
}

Забытый background-repeat

Часто используя большое фоновое изображение, мы забываем учитывать тот случай, когда страница просматривается на большом экране. По умолчанию, фон будет повторяться.

Это не видно на экране ноутбука, но хорошо видно на экранах большего размера.

Забытый background-repeat

Чтобы заранее избежать такого поведения, обязательно сбросьте значение background-repeat.

.hero {
background-image: url('..');
background-repeat: no-repeat;
}

Вертикальные медиа-запросы

Иногда так заманчиво создать компонент и протестировать его изменяя только ширину окна браузера. Тестирование с учётом высоты браузера может выявить некоторые интересные проблемы.

Вот одна из них, с которой я сталкивался несколько раз. У нас есть дополнительный компонент с основными и второстепенными ссылками.

Рассмотрим следующий пример. Основная и дополнительная навигация выглядят нормально. В примере, который я видел, разработчик добавил position: sticky для вторичной навигации, что бы она могла прилипать к низу.

position: sticky

Однако, если высота браузера меньше, всё ломается. Обратите внимание, как два навигационных блока перекрываются.

Если высота браузера меньше, всё ломается.

Используя вертикальный медиа-запрос CSS, мы можем избежать этой проблемы.

@media (min-height: 600px) {
.aside__secondary {
position: sticky;
bottom: 0;
}
}

Таким образом, вторичная навигация будет прикреплена к низу только в том случае, если высота области просмотра больше или равна 600px. Намного лучше, правда?

Вероятно есть более эффективные способы реализовать это поведение (например, с помощью margin-top: auto), но в этом примере я сосредоточусь на вертикальном медиа-запросе.

Если я захочу объяснить использование вертикального медиа-запроса CSS, мне нужно написать об этом целую статью. Хорошая новость в том, что я уже написал её, если вам интересно.

Использование justify-content: space-between

В контейнере flex вы можете использовать justify-content, чтобы отделить дочерние элементы друг от друга. С определённым количеством дочерних элементов макет будет выглядеть нормально. Однако, когда их становиться больше или меньше макет выглядит странно.

Рассмотрим следующий пример.

justify-content: space-between

У нас flex контейнер с четырьмя дочерними элементами. Расстояние между каждым дочерним элементом — это не gap или margin, он оно есть потому что в контейнере указано свойство justify-content: space-between.

.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}

С количеством дочерних элементов меньше четырёх, произойдёт вот что.

С количеством дочерних элементов меньше четырёх, произойдёт вот что.

Выглядит нехорошо. Есть разные решения для этого:

Для простоты я буду использовать gap.

.wrapper {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
Используем gap: 1rem;

Текст поверх изображения

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

Вот пример.

Текст поверх изображения

Текст выглядит читабельным, но только когда загрузилось изображение.

Текст выглядит читабельным, но только когда загрузилось изображение.

Это легко исправить добавив фон к элементу <img>. Фон будет видно только в том случае, если изображение не загрузилось. Разве это не круто?

.card__img {
background-color: grey;
}
Добавим фон к изображению

Будьте осторожны с Фиксированными значениями CSS Grid

Допустим у нас есть Grid, который содержит aside и main. CSS выглядит так:

.wrapper {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}

Это сломается при небольших размерах области просмотра из-за нехватки места. Чтобы избежать такой проблемы, всегда используйте медиа-запросы при использовании CSS Grid, как показано ниже.

@media (min-width: 600px) {
.wrapper {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}
}

Показывайте полосу прокрутки только когда это необходимо

К счастью, мы можем управлять отображением полосы прокрутки показывать её или нет в случае длинного контента. При этом настоятельно рекомендуется использовать auto в качестве значения overflow.

Рассмотрим следующий пример.

overflow-y: scroll

Обратите внимание, что даже если контента мало, полоса прокрутки видна. Это плохо для пользовательского интерфейса. Как дизайнера меня смущает видимая полоса прокрутки, когда в ней нет необходимости.

.element {
overflow-y: auto;
}

С overflow-y: auto полоса прокрутки будет видна только в том случае, если контента много. Иначе её там не будет. Вот обновлённый рисунок.

overflow-y: auto

Место для полосы прокрутки

Ещё одна вещь, связанная с полосой прокрутки, — это место для полосы прокрутки (дословно жёлоб полосы прокрутки). Если взять предыдущий пример, когда контент становится длиннее, появление полосы прокрутки вызовет сдвиг макета. Резервирование места для полосы прокрутки поможет избежать сдвига макета.

Рассмотрим следующий рисунок.

Место для полосы прокрутки

Обратите внимание, как содержимое смещалось, когда оно становилось длиннее в результате отображения полосы прокрутки. Мы можем избежать такого поведения, используя свойство scrollbar-gutter.

.element {
scrollbar-gutter: stable;
}
scrollbar-gutter: stable;

Минимальный размер содержимого CSS Flexbox

Если flex элемент содержит текст или изображение, размер которого больше, чем длинна элемента, браузер не сжимает их. Это поведение по умолчанию для Flexbox.

Рассмотрим следующий пример.

.card {
display: flex;
}

Когда в заголовке содержится очень длинное слово, оно не переносится на новую строку.

Когда в заголовке содержится очень длинное слово, оно не переносится на новую строку.

Даже если мы используем overflow-break: break-word, это не сработает.

.card__title {
overflow-wrap: break-word;
}

Для изменения такого поведения по умолчанию, на нужно установить min-width элемента равную 0. Это связано с тем, что значение по умолчанию для min-widthauto, происходит переполнение.

.card__title {
overflow-wrap: break-word;
min-width: 0;
}

То же самое применимо и для столбца flex, но используется min-height: 0.

min-height: 0

Минимальный размер содержимого в CSS Grid

Подобно Flexbox, CSS Grid имеет минимальный размер для содержимого своих дочерних элементов равный auto. Это значит, что если есть элемент больше элемента Grid, будет переполнение.

Минимальный размер содержимого в CSS Grid

В приведённом выше примере, у нас есть карусель в разделе main. Вот HTML и CSS, для контекста.

<div class="wrapper">
<main>
<section class="carousel"></section>
</main>
<aside></aside>
</div>
@media (min-width: 1020px) {
.wrapper {
display: grid;
grid-template-columns: 1fr 248px;
grid-gap: 40px;
}
}

.carousel {
display: flex;
overflow-x: auto;
}

Поскольку карусель это flex контейнер, который не переносится. Его ширина больше, чем у секции main, элемент grid это учитывает. В результате появляется полоса прокрутки.

Чтобы исправить это, у нас есть три разных решения:

В качестве защитного CSS механизма я бы выбрал первый, который использует функцию minmax().

@media (min-width: 1020px) {
.wrapper {
display: grid;
grid-template-columns: minmax(0, 1fr) 248px;
grid-gap: 40px;
}
}
В качестве защитного CSS механизма я бы выбрал первый, который использует функцию minmax()

Я писал об этом в начале года. Я также настоятельно рекомендую посмотреть «Preventing a Grid Blowout» и «You want minmax(10px, 1fr) not 1fr»Криса Койера (Chris Coyier).

auto-fit vs auto-fill

Когда используется GSS Grid функция minmax() важно выбрать правильно между используемыми ключевыми словами auto-fit или auto-fill. При неправильном использовании это может привести к неожиданным результатам.

При использовании функции minmax(), ключевое слово auto-fit будет расширять элементы Grid, что бы заполнить всё доступное пространство. В то время как auto-fill сохраняет доступное пространство зарезервированным без изменения ширины элементов Grid.

auto-fit vs auto-fill

При этом использование auto-fit может привести к тому, что элементы Grid будут слишком широкими, особенно когда они меньше ожидаемого. Рассмотрим следующий пример.

.wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 1rem;
}

Если есть только один элемент Grid и используется auto-fit, элемент будет расширяться, что бы заполнить ширину контейнера.

auto-fit

В большинстве случаев в таком поведении нет необходимости поэтому на мой взгляд, лучше использовать auto-fill.

.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
auto-fill

Максимальная ширина изображения

Не забывайте устанавливать max-width: 100% для всех изображений. Это можно добавить к CSS сбросу, который вы используете.

img {
max-width: 100%;
object-fit: cover;
}

position: sticky в CSS Grid

Вы когда-нибудь пробовали использовать position: sticky с дочерним элементом Grid контейнера? По умолчанию элементы Grid растягиваются. В результате элемент aside, в нижнем примере равен по высоте секции main.

position: sticky

Что бы он работал правильно, вы нужно сбросить свойство align-self.

aside {
align-self: start;
position: sticky;
top: 1rem;
}
Нужно сбросить свойство align-self

Я подробно писал на эту тему в своём блоге, если вам интересно. Или в статье «Прилипающие CSS Grid элементы»

Группировка селекторов

Не рекомендуется группировать селекторы, предназначенные для работ с разными браузерами. Например, для стилизации атрибута placeholder требуется несколько селекторов для каждого браузера. Если мы сгруппируем селекторы, всё правило будет не действительным, согласно w3c

/* Не делайте так, пожалуйста */
input::-webkit-input-placeholder,
input:-moz-placeholder
{
color: #222;
}

Вместо этого, делайте так.

input::-webkit-input-placeholder {
color: #222;
}

input:-moz-placeholder {
color: #222;
}

Это не конец

Это ещё не конец, но мне очень понравилось документировать все эти техники. Это постоянный список защитных приёмов CSS, которые я лично использую в зависимости от проекта, над которым работаю. Если вам есть что предложить, свяжитесь со мной через twitter @shadeed9.

Дополнительные материалы

Предыдущая Статья

JavaScript: Что такое hoisting

Следующая Статья

Объяснение JSON простым языком