Проблемы CSS

Впервые css был представлен примерно в 1995 году, и был предназначен для стилизации простых текстовых документов. Не веб сайтов. Не приложений. А именно текстовых документов. С тех пор, css, прошел долгий путь. Возможно слишком долгий.

Для многих вещей, css, не был предназначен изначально, например для таких как: многоколоночность, отзывчивый веб дизайн и т.д. Вот почему он стал языком полным хаков и глюков, как какая-то древняя машина с кучей расширений.

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

bf00a25bae235e3ea9a9936cdfad89ca

Перейдем к сути, ведь я здесь не для того, что бы высказать свое мнение о css. А для того, что бы рассмотреть ряд часто встречающихся в css проблем, и их возможных решений.

Я старался выбрать несколько действительно очень распространенных:

  • Очистка float`ов.
  • Как победить отступы между элементами с inline-block?
  • Понимание абсолютного позиционирования.
  • Когда использовать width / height равный 100% (часть 2)
  • Как не облажаться с z-index. (часть 2)
  • Что такое свертывание границ(margin collapsing)? (часть 2)

Очистка float`ов.

Мне кажется, что это самый распространенный вопрос в css. Он стар как мир, честно, я на тысячу процентов уверен, что каждый, кто хоть когда нибудь писал на css сталкивался с ним.

Простыми словами его можно описать так: когда внутри элемента содержатся только элементы с float, он схлопывается так, как будто в нем вообще нет дочерних элементов. То есть, по факту, элементы с float выпадают из общего потока.

5f48804c4ec8f81eefa52c2fe42d8ca2

Есть несколько способов для решения этой проблемы. Раньше мы использовали пустой div со стилем «clear: both» в самом низу контейнера. Затем, мы заменили его на тег hr, что не на много лучше.

И наконец Nicolas Gallagher предложил новый путь очистки float`ов без необходимости трогать разметку совсем. После продолжительных дискуссий и тестов, мы получили минимально необходимый, для его работы, набор стилей, вот его последняя версия:

.clearfix:after {
    content: "";
    display: table;
    clear: both;
}

 

На самом деле я солгал, она не последняя, она самая короткая. Но если вам нужна поддержка IE 6/7, то вам нужно добавить еще вот это:

.clearfix {
    *zoom: 1;
}

 

Для работы необходимо добавить класс .clearfix в свой проект, а затем применять его к элементам разметки. Это самый простой и чистый способ для работы с float`ами.

Как победить отступы между элементами с inline-block?

Продолжим с размещением элементов в строку, в этот раз не с помощью float, а с помощью inline-blocks. display: inline-block долгое время был недооценен, и все же мы разобрались, как он работает и почему это круто. Сегодня, все больше и больше front-end разработчиков предпочитают использовать inline-block взамен float`ов, когда у них есть такая возможность.

Главный плюс inline-block в том, что нам не приходится отчищать float`ы и мы не сталкиваемся с другими проблемами которые могут возникнуть из-за элементов спозиционированных с помощью float`ов. Просто установив свойство элемента display в значение inline-block получим гибрид строчного элемента и блока. Они могут иметь размер, отступы, но их ширина, по-умолчанию, зависит от контента, а не занимает всю ширину родителького элемента. Таким образом они размещаются горизонтально, а не вертикально.

Вы можете спросить: «В чем же тогда проблема?». А проблема в том, что они на половину строчные, а значит имеют отступ друг от друга размером равным пробелу. Со стандартным шрифтом размером 16px, он составляет 4px. В большинстве случаев размер отступа можно рассчитать, как 25% от размера шрифта. Так или иначе, это может помешать нормальному расположению элементов. Например, возьмем контейнер размером в 600px с тремя элементами внутри, размер которых 200px и задано свойством display: inline-block. Если не убрать отступ, то нам не удастся разместить их в одну линию (200 *3 + 4 * 2 = 608).

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

Уровень разметки: удаление пробелов

Для всех наших тестов воспользуемся следующей разметкой.

<div class="parent"> <!-- 600px -->
    <div class="child">I'm a child!</div> <!-- inline-block 200px -->
    <div class="child">I'm a child!</div> <!-- inline-block 200px -->
    <div class="child">I'm a child!</div> <!-- inline-block 200px -->
</div>

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

<div class="parent">
    <div class="child">I'm a child!</div><div class="child">I'm a child!</div><div class="child">I'm a child!</div>
</div>

Это действительно работает, но делает не удобным чтение нашего кода. Хотя, мы можем, немного реорганизовать наши элементы, что бы сохранить читаемость:

<div class="parent">
    <div class="child">
        I'm a child!</div><div class="child">
        I'm a child!</div><div class="child">
        I'm a child!</div>
</div>

И если быть уж совсем крутым, можно сделать и так:

<div class="parent">
    <div class="child">I'm a child!</div
  ><div class="child">I'm a child!</div
  ><div class="child">I'm a child!</div>
</div>

Да — это работает! Конечно я не рекомендую такой подход, потому что он не интуитивен и делает код уродливым. Давайте, лучше, попробуем, что нибудь еще.

 

Уровень разметки: комментируем пробелы

Что если закомментировать пробелы вместо того, что бы удалить их?

<div class="parent"> <!-- 600px -->
     <div class="child">I'm a child!</div><!-- 
--><div class="child">I'm a child!</div><!--
--><div class="child">I'm a child!</div>
</div>

Да, так гораздо лучше! Код читаем и работает. Да, способ выглядит не привычно на первый взгляд, но не так и сложно к нему привыкнуть. Я и сам использую такой способ, когда мне надо удалить пробелы между элементами с inline-block.
Конечно, кто-то скажет, что это не идеальное решение, так как оно работает на стороне разметки, а проблема должна быть решена на уровне css. Действительно. Большинство из нас на самом деле используют css решения.

Уровень css: расстояние между символами

Свойство letter-spacing используется для задания отступов между символами. Идея заключается в том, что бы установить отступ таким, что бы он нивелировал отступ между нашими элементами, затем, нам придется сбросить letter-spacing для дочерних элементов, что бы текст в них выглядил нормально.

.parent {
    letter-spacing: -0.3em;
}

.child {
    letter-spacing: normal;
}

Эта техика используется в Griddle — основанной на Sass системе сеток за авторством Nicolas Gallagher, так что, как вы видите, это достаточно серьезное решение. Хотя, честно говоря, мне не нравится тот факт, что мы полагаемся на магичиские числа в стилях. Плюс с некоторыми шрифтами, это число может меняться например на 0.31em и т.д. То есть, его необходимо подгонять под каждый конкретный случай.

 

Уровень css: отрицательный margin

Еще один подход к решению задачи, очень похож на предыдущий, но с использование отрицательного отступа. Главный его недостаток он не работает в IE 6/7. Плюс нам необходимо убрать отступ с первого элемента, что бы они ровно встали внутри нашего контейнера.

.child {
    margin-left: -0.25em;
}

.child:first-of-type {
    margin-left: 0;
}

Если вам не требуется поддержка IE 6/7, я считаю что это достаточно неплохе решение.

Уровень css: font-size

Ну и на последок, вы можете установить размер шрифта родительского блока в 0, что бы сделать пробел равным 0px, а затем восстановить размер шрифта для дочерних элементов.

.parent {
    font-size: 0;
}

.child {
    font-size: 16px;
}

У этого решения есть несколько своих проблем и ограничений:

Так что, это не лучшее решение. Как я уже говорил ранее, скорее всего буду использовать путь с комментирование пробелов. Если для вас он выглядит неудобным, вы можете вернуться к float`ам или же вообще использовать flexbox.

Понимание абсолютного позиционирования.

Позиционирование элементов — каверзный процесс и всегда им был. Позиционирование, начинающим, дается с большим трудом. Они часто (не)используют свойство position. Это свойство определяет как элемент может перемещаться с помощью смещений (top, right, bottom and left). И принимает следующие значения:

  • static — по-умолчанию, смещения не действуют
  • relative — смещения двигают визуальный слой, но не сам элемент
  • absolute — смещения двигают элемент внутри контекста (первый не static элемент)
  • fixed — смещения позиционируют элемент внутри viewport`a и не важно где он расположен в документе

Проблемы появляются при использовании position: absolute. И наверняка вы с ними уже сталкивались: вы определили элемент с абсолютным позиционирование, потому что хотите, что бы он был в верхнем правом углу своего родителя (например кнопка закрыть у модального окна).

element {
    position: absolute;
    top: 0;
    right: 0;
}

 
… а он оказывается в верхнем правом углу документа. У вас промелькает мысль «Какого черта?». На самом деле, это нормальное поведение браузера. Ключевое слово тут контекст.

Код выше просто говорит: «Я хочу что бы мой элемент был спозиционирован в верхнем правом углу контекста». Так что же такое контекст? Это первый элемент со свойством position не равным static. Это может быть непосредственно родительский элемент, или родитель родителя, или родитель родителя родителя. И так до первого элемента с position != static.

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

Небольшая демка иллюстрирующая вышесказанное. Два родителя, в каждом по одному дочернему элементу спозиционированному абсолютно со смещение top: 0 и right: 0. Слева правильный родитель с position: relative, справа неправильный с position: static.

jsFiddle

Когда использовать width / height равный 100%?

Height: 100%

Пожалуй начнем с того, что по проще. Когда использовать height: 100%? На самом же деле, вопрос часто звучит немного по-другому: «Как мне сделать так, что бы моя страница заняла всю высоту экрана?». Ведь правда?

Для ответа на него нужно понять, что height: 100% равен высоте родительского элемента. Это не магическое «высота всего окна». Так что, если вы захотите, что бы ваш элемент занял все 100% от высоты окна, то установить height: 100%будет недостаточно.

Почему? А потому что родителем вашего контейнера является элемент body, а у него свойство height установлено в auto по-умолчанию, это значит, что его высота равна высоте контента. Конечно, вы можете попробовать добавить height: 100%к body, но этого тоже будет недостаточно.

Почему? А все по тому же, родителем элемента body является элемент html, у которого, так же, свойство height равно auto и он так же растягивается под размер контента. А вот теперь, если добавить height: 100% и к элементу html, то все заработает.

Стало понятнее? Корневой элемент html на самом деле не самый верхней уровень на странице, самый верхний это «viewport». Для простоты, будем считать, что это окно браузера. Так вот, если установить height: 100% элементу html, то это то же самое, что сказать — стань такой же высоты, как окно браузера.

Суммируем полученную информацию в небольшом кусочке кода:

html, body, .container {
    height: 100%;
}

 
Готово. Если вам интересно углубится в тему, как устроен viewport, я настоятельно рекомендую статью от PPK.

А что если у родительского элемента установлено свойство min-height, а не height?

Недавно, Роджер Йохансен (Roger Johansson) описал проблему с height: 100%, проявляющуюся, когда у родительского элемента не установлен height, но указан min-height. Я не хочу углубляться в сказанное в статье, а перейду сразу к выводам. Вам необходимо установить height: 1px для родителя, что бы дочерний элемент смог занять всю высоту указанную в min-height.

.parent {
    min-height: 300px;
    height: 1px; /* Required to make the child 100% of the min-height */
}

.child {
    height: 100%;
}

 
Пример на jsFiddle.

Более подробно, с этим вопросом, вы можете ознакомится в статье Роджера Йохансена (Roger Johansson).

Width: 100%

Теперь давайте разберемся с width: 100%. Для начала, небольшое уточнение: устанавливая свойство width: 100%, мы хотим, что бы наш элемент занял всю ширину родительского элемента. Все стандартно.

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

Если добавить padding и/или border к элементу с width: 100%, то он перестанет помещаться в родительский элемент. Потому что появились padding и border, вот почему width должен был называться content-width. А теперь, пожалуйста, посмотрите на пример демонстрирующий вышесказанное.

Пример на jsFiddle.

Допустим, родитель имеет ширину 25em, а дочерний элемент, ширину 100% (от ширины родителя), но он так же имеет padding равный 1em (1em справа и 1em слева, в сумме 2em по горизонтали) и border размером 0.5em (0.5em справа и 0.5em слева, в сумме 1em по горизонтали), что в итоге нам дает 25em (100%) + 2em + 1em = 28em.

Есть 4 возможных пути решения этой проблемы. Первый и наверное лучший способ — избегать свойства width: 100%, тем более, что в данном случае оно абсолютно бесполезно. Если дочерний элемент блочный, то он и так займет всю ширину родителя автоматически (без проблем с padding`ами и border`ами). Но если мы работаем с inline-block элементом, то нам не удастся так просто решить эту проблему.

Мы можем заменить width: 100% на статичный размер. В нашем случае 25 — (2 + 1) = 22em. Само собой — это плохое решение, потому что нам нужно вычислять ширину вручную. Пойдем другим путем!

Третий способ — использовать calc() для расчета ширины: width: calc(100% — 3em). Но оно тоже не подходит. Во-первых, нам все еще нужно вычислять размеры padding + border. Во-вторых, calc() не то, что бы хорошо поддерживался браузерами (не работает в IE 8, Safari 5, Opera 12, родном браузере Android).

Идея номер четыре — использовать свойство box-sizing: border-box. Оно изменяет алгоритм расчета ширины и высоты элемента так, что бы в них учитывались свойства padding и border. Отличная новость, заключается в том, что у box-sizingхорошая поддержка браузерами (IE8+, Opera 7+). А для всех остальных браузеров можно использовать polyfill.

Вывод: не используйте width: 100% без box-sizing: border-box.

Как не облажаться с z-index.

Все элементы на страницы позиционируются в трех плоскостях: кроме вертикальной и горизонтальной оси, существует дополнительная ось Z (глубина). По началу все выглядит очень просто — элементы с большим z-index находятся выше элементов с меньшим z-index. К несчастью, все гораздо сложнее. Я уверен, что z-index самое сложное css свойство за всю его историю. А так же уверен, что проблемы связанные с z-index встречаются чаще других при работе с css. Надеюсь, что мы просветим возможные пути их решения.

Для начала. Свойство z-index не имеет эффекта на статических элементах. Что бы иметь возможность перемещать элемент по оси Z, нам нужно изменить его позиционирование на relative, absolute или fixed.

Следующие, что нужно знать о z-index — это то, что не все элементы в DOM дереве размещены на одном уровне. Это значит, что изменение z-index у элемента до очень большого значения, не гарантирует того, что элемент будет помещен на передний план. Это называется контекстом наложения.

Простыми словами, контекст наложения является, своего рода, группой на основе одного html элемента, у которого все дочерние элементы получают ту же позицию в контексте и такой же z-index. Изменения z-index у элемента может привести к перекрыванию им других элементов, так как вам необходимо. Вот как располагаются элементы в одном контексте наложения (снизу вверх):

  1. Фон и границы элемента, формирующего контекст
  2. Дочерние контексты наложения с негативным z-index (самый маленький первый)
  3. Не позиционированные элементы
  4. Позиционированные элементы с значением z-index равным auto или 0
  5. Позиционированные элементы с положительным z-index

 

Когда ситуация становится неприятной

Итак, мы рассмотрели основы z-index понимание которых сэкономит вам кучу времени, уж поверьте. Но, к сожалению, их недостаточно. Это было бы слишком просто.

Дело в том, что каждый контекст наложения имеет свою ось Z. Например, элемент A в контексте наложения 1 и элемент B в контексте наложения 2 не могут взаимодействовать через z-index. Это значит, что элемент A, как часть контекста наложения находящегося в самом низу общего контекста, ни когда не сможет перекрыть элемент B другого контекста, находящегося выше уровнем, даже с очень большим значением z-index.

Но, что еще хуже. Элемент html создает корневой контекст наложения. Затем, каждый не статично-спозиционированный элемент со свойством z-index не равным auto, так же создает свой контекст наложения. Ничего нового. Но вот где все начинает рушиться: некоторые, ни как не связанные с контектом наложения css свойства, так же создают новые контексты. Например свойство opacity.

Все верно, свойство opacity создает новый контекст наложения. То же самое делают свойства transform и perspective. Хотя это не имеет ни какого смысла, не так ли? Например, если у вас есть какой-нибудь элемент с opacity меньше 1 или с любой трансформацией, у вас потенциально может возникнуть проблема.

К сожелению каждая проблема с z-index имеет свой контекст (не каламбур) делающий невозможным универсальное решение.

Давайте подведем краткий итог вышесказанного:

  • Перед применением z-inedx убедитесь, что установили свойство position не равным static
  • Не используйте значения большем чем 5 цифр для z-index, это абсолютно бессмысленно; в большинстве случаев, значение z-index в районе 10, будет более чем достаточно
  • Убедитесь, что элемент который вы хотите перекрыть находится в том же контексте наложения
  • Если у вас все еще что-то работает не так, как должно, убедитесь в отсутствии трансформаций и opacity выше у родительских элементов

В тему, я так же рекомендую к прочтению What No One Told You About Z-index от Филипа Волтона (Philip Walton) и официальную спецификацию css.

Борьба со схлопыванием отступов

Как мне кажется — это один из глюков css, который крадет наибольше количество времени, что бы разобраться в чем же дело. Можно сказать, что он чем-то похож на баг с z-index. Как-бы то ни было, схлопывание отступов — это когда верхний и нижний отступ двух элементов схлопываются в один (самый большой из двух).

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

Сосдение элементы

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

  • clear: left; float: left; (right то же работает)
  • display: inline-block;

Пример на jsFiddle иллюстрирует работу фиксов.

Родитель и первый/последний дочерний элемент

Обычно, верхний отступ родительского элемента и верхний отступ дочернего схлопываются до самого большого. Аналогичным образом работает и для последнего дочернего элемента и нижних отступов. Для решения этой проблемы, так же есть несколько способов. Большинство из них заключаются в добавлении одого из следующих свойств родительскому элементу:

  • overflow: hidden (или любой другой но не auto)
  • padding: 1px (или другое значение больше 0)
  • border: 1px solid transparent (или любой другой border)
  • float: left (right то же работает)

Пример на jsFiddle иллюстрирует работу фиксов.

Пустые блоки

Когда пустой блок не имеет границ, padding`ов, высоты, его верхние и нижние отступы схлопываются в один. Так или иначе использовать пустые блоки плохая практика, так что такое встречается не часто.

Заключение

К несчастью, тут описана только верхушка айзберга из багов и хаков css. Но это действительно те, которые встречаются чаще других, но есть еще множество вещей таких как: несовместимость браузеров, вендорные префиксы, специфичность селекторов, каскады и наследование, и многое многое другое.

Также, я бы рекомендовал к ознакомлению следующие статьи и сайты:

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