Pull to refresh

Количественные CSS селекторы

Reading time 7 min
Views 91K
Original author: Heydon Pickering
Вам когда-нибудь хотелось прямо в CSS коде посчитать, в меню 4 элемента или 10? Для четырех задать им ширину по 25%, а если набралось десять — прижать их друг к другу и уменьшить отступы?
Как выяснилось, CSS умеет работать с разным количеством элементов, позволяя избавиться от головных болей и лишнего кода на js.




Динамичный контент


Респонсив дизайн обычно ассоцируется с одной переменной — пространством. Когда мы тестируем респонсив сетки, мы берем какой-то объем контента и смотрим, сколько места он займет, как это выглядит и куда он влазит, а куда нет. То есть, контент считается константой, а пространство — переменной.

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

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

В этой статье я опишу технику создания CSS, лишенного проблем с количеством (подсчётом) элементов. Достигается это за счёт специально оформленных селекторов. Я буду применять их в контесте решения классической проблемы: как разделить вёрстку элементов в горизонтальном меню сайта, когда в нём мало и много элементов. Например, как добиться того, что при 6 и более пунктах меню элементы имели стиль display: inline-block, а при меньшем количестве — display: table-cell.

Я не буду использовать шаблонизацию или js, мне даже не понадобятся прописывать классы у элементов меню. Техника использования только CSS селекторов соответствует принципу разделения интересов, согласно которому, контент (HTML) и отображение (CSS) имеют ясно обозначенные роли. Разметка — это работа CSS и, по возможности, одного только CSS.



Демо доступно на CodePen и еще будет упоминаться по ходу статьи.

Чтобы упростить понимание, я буду использовать картинки с кальмарами вместо HTML тегов. Зеленые кальмары будут за элементы, которые попадают под конкретный селектор, красные — за те, которые не попадают, а серые будут значить, что элемента не существует.



Счёт


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

Считаем до одного


Селектор :only-child срабатывает на элементах, которых всегда одна штука. По сути, это позволяет нам «применить стили ко всем дочерним элементам конкретного элемента, если суммарно их ровно 1». Это единственный простой селектор, который можно описать как «подсчитывающий» (кроме, конечно, похожего на него :only-of-type).

В описанном ниже примере я использую :only-of-type, чтобы применить стили ко всем кнопкам, которые являются единственными чилдами-кнопками среди их соседей.

button { 
  font-size: 1.25em;
}

button:only-of-type {
  font-size: 2em;
}

Важно понимать, что очередность строк здесь имеет ключевое значение. Кроме этого, полезно видеть этот код с точки зрения селекта «меньше-чем-два»:



Аналогично, мы теперь можем сделать селект «больше-чем-один», используя отрицание.

/* "больше-чем-один" будут иметь уменьшенный размер шрифта */
button {
  font-size: 2em;
}

button:not(:only-of-type) {
  font-size: 1.25em;
}



N элементов


Применять стили, основываясь на «больше-чем-один» и «меньше-чем-два» это ловкий трюк, но нам нужен более гибкий инструмент, позволяющий оперировать с любым числом. Мы хотим использовать селекты “больше или равно N” для любого N. Кроме этого, хочется иметь селект «ровно 745» или «суммарно ровно 6».

Для этого у нас есть селектор :nth-last-child(n), в который мы параметром передаем любое число. Он позволяет отсчитать столько-то элементов назад от конца списка. Например, :nth-last-child(6) выберет элемент, который является шестым с конца среди своих соседних элементов.

Всё становится интереснее, когда мы совмещаем :nth-last-child(6) и :first-child, в результате получая все элементы, которые являются шестыми с конца и при этом первыми с начала.

li:nth-last-child(6):first-child {
  /* зеленый кальмар */
}

Если такой элемент существует, это будет значить, что у нас ровно 6 элементов. Таким образом, я написал CSS код, который может сказать, сколько элементов я вижу перед собой.



Осталось теперь воспользоваться этим одновременно «шестым с конца и первым с начала» элементом, чтобы еще и поселектать все остальные 5 элементов. Для этого я воспользуюсь общим соседним комбинатором.



Если вы не знакомы с этим комбинатором, то объясняю, ~ li в селекте li:nth-last-child(6):first-child ~ li значит «любой li, который идёт после li:nth-last-child(6):first-child». В нашем случае, элементы будут иметь зеленый цвет шрифта, если их ровно 6 штук.

li:nth-last-child(6):first-child, 
li:nth-last-child(6):first-child ~ li {
  color: green;
}

Больше или равно 6


Селектать фиксированное количество, будь то 6, 19 или 653 — не очень-то полезно, так как подобная необходимость очень редка. Это как в media queries — не очень удобно использовать фиксрованную ширину вместо min-width или max-width:

@media screen and (width: 500px) {
  /* стили для ширины вьюпорта ровно в 500px */
}

В навигационном меню я хочу переключать стили с границей, основанной на количестве элементов, например, поменять нужные стили, если у меня 6 и больше элементов (а не ровно шесть).

Вопрос в том, как сделать такой селектор? и это вопрос смещений.

Параметр n+6


Селектор :nth-child() может принимать параметром не только число, но и формулу “n + [число]”. Например, :nth-child(n+6) будет применён ко всем элементам, начиная с шестого.


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

Чтобы обойти эту проблему, нам нужно создать такой селект, который выделит все элементы, кроме последних пяти. Используя обратный к :nth-child(n+6) селект :nth-last-child(n+6) мы сможем выделить все элементы «от с шестого с конца и до самого первого с начала».

li:nth-last-child(n+6) {
  /* здесь стили */
}

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



Если же в списке шесть или больше элементов, нам остаётся прибавить к селекту оставшиеся пять элементов. Это легко — если элементов больше шести, то срабатывает условие nth-last-child(n+6), и комбинируя его с "~" мы сможем поселектать все нужные нам элементы.



Такая вот короткая запись и является решением нашей проблемы:

li:nth-last-child(n+6),
li:nth-last-child(n+6) ~ li {
  /* здесь стили */
}

Разумеется, тут может быть любое положительное целое число, даже 653,279.

Меньше или N


Как и в предыдущем примере с :only-of-type, селектор можно использовать в обе стороны, и как «больше или равно N» и как «меньше или равно N». Какой вариант вы будете использовать? Зависит от того, какую сетку вы будете считать основной.
В случае «Меньше или N» мы берем n с минусом и добавляем условие :first-child.

li:nth-last-child(-n+6):first-child,
li:nth-last-child(-n+6):first-child ~ li {
  /* здесь стили */
}

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

nth-child против nth-of-type


Заметьте, что в предыдущих примерах я использовал :nth-child() и :nth-last-child(), а не :nth-of-type() с :nth-last-of-type(). Так как я селектал теги <li> и правильными потомками <ul> могут быть только они, :last-child() и :last-of-type() оказываются одинаково результативны.

Обе группы селектов :nth-child() и :nth-of-type() имеют свои преимущества, исходя из того, чего вы хотите добиться. Так как :nth-child() не привязан к конкретному тегу, описанную выше технику можно применять к смешанным дочерним элементам:

<div class="container">

  <p>...</p>

  <p>...</p>

  <blockquote>...</blockquote>

  <figure>...</figure>

  <p>...</p>

  <p>...</p>

</div>


.container > :nth-last-child(n+3),
.container > :nth-last-child(n+3) ~ * {
  /* здесь стили */
}

(Обратите внимание, чтобы не привязываться к тегам, я использую универсальный селектор. :last-child(n+3) ~ *. В этом случае, он обозначает «любой элемент любого тега, следующий за :last-child(n+3).»

С другой стороны, преимущество :nth-last-of-type() в том, что вы можете селектать элементы с одним тегом среди множества других соседних. Например, можно посчитать количество тегов <p>, несмотря на то, что они в одной куче с <div> и <blockquote>.

<div class="container">

  <div>...</div>

  <p>...</p>

  <p>...</p>

  <p>...</p>

  <p>...</p>

  <p>...</p>

  <p>...</p>

  <p>...</p>

  <blockquote>...</blockquote>

</div>


p:nth-last-of-type(n+6),
p:nth-last-of-type(n+6) ~ p {
  /* здесь стили */
}

Поддержка браузерами



Все используемые в статье селекторы CSS2.1 и CSS3 поддерживаются в Internet Explorer 9 и выше, а так же все современные мобильные и десктопные браузеры.

Internet Explorer 8 неплохо поддерживает большинство селектов, но задуматься о полифилле на js не помешает. В качестве альтернативы, можно расставить специальные классы для старых версий IE и приписать их к своим селекторам. В случае с навигационным меню, код будет выглядеть примерно так:

nav li:nth-last-child(n+6),
nav li:nth-last-child(n+6) ~ li, 

.lt-ie9 nav li {
  display: inline-block;
  /* и т.д. */
}

В реальном мире



Предположим, наше навигационное меню принадлежит сайту с CMS. В зависимости от того, кто его наполняет и использует, в меню будет разное количество элементов. Кто-то старается не усложнять и ему хватает «Home» и «About», а кто-то хочет запихать в меню всё, что есть у него на сайте.

Создавая стили, учитывающие разные сетки для разного количества элементов в меню, мы делаем вёрстку более стройной и подходящей для самых разных форматов — какой бы ни был контент, у нас всё учтено.



Поздравляю, теперь в вашем CSS арсенале есть умение оперировать количеством элементов.

Контент-независимый дизайн


Респонсив дизайн решает одну важную проблему: он позволяет комфортно расположить один и тот же контент на множестве разных устройств. Было бы неприемлемо видеть разный контент только потому, что у тебя экран неправильного размера. Так же и неприемлемо диктовать дизайнеру, под сколько пунктов меню должен быть заточен сайт. «Не рисуй так, у нас вся сетка поедет, если будет столько элементов».

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

Веб дизайн это всегда изменчивость, гибкость и неопределенность. В нём скорее не знаешь, чем знаешь. Он уникален тем, что представляет с собой вид визуального дизайна, который не определяет формы, а предвидит, какие формы может что-то принять. Для кого-то это неприемлемо и непонятно, но для вас и меня это вызов и удовольствие.
Tags:
Hubs:
+57
Comments 24
Comments Comments 24

Articles