Пользователь
0,0
рейтинг
5 марта 2015 в 13:23

Разработка → Количественные CSS селекторы перевод

Вам когда-нибудь хотелось прямо в 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 арсенале есть умение оперировать количеством элементов.

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


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

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

Веб дизайн это всегда изменчивость, гибкость и неопределенность. В нём скорее не знаешь, чем знаешь. Он уникален тем, что представляет с собой вид визуального дизайна, который не определяет формы, а предвидит, какие формы может что-то принять. Для кого-то это неприемлемо и непонятно, но для вас и меня это вызов и удовольствие.
Перевод: Heydon Pickering
@alspaladin
карма
15,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (24)

  • +11
    Я вас разочарую, но я об этом год с лишним назад писал, и даже там мне указали, что идея вторична :)
    habrahabr.ru/company/uprock/blog/198322/
    • +7
      И если говорить серьезно — использование хотя бы одного nth-child (и тем более nth-last-child) в стилях включают более сложный механизм работы css-селекторов. Лучше отказаться, наконец, от поддержки ie8 и 9(только 9й поддерживать на самом деле бред — у него доля даже меньше, чем у восьмого), и перейти на flex-box. Где есть justify-content: space-around/space-between. Я вообще сейчас с react заигрываю, там вообще инлайновые стили приветствуются, а значит — можно точно узнать количество потомков через js.

      А такие костыли лучше использовать разве что для таких гемморойных и странных задач, как расстановка элементов равномерно по кругу (в моем посте есть такой пример)
      • 0
        Про флексбокс согласен, а инлайновые стили это всё-таки совсем другая степь, их сложнее поддерживать, переносимость и повторяемость тоже хуже, чем у таких вот решений.
        • 0
          Ну, там есть и немного более специфичное решение, нежели просто «инлайн». Просто стили пишутся в JS, а не в CSS-файле.
          Ну и с поддержкой-переносимостью-повторяемостью в нем тоже все решили по большому счету)
      • 0
        Что касается nth-child и скорости, насколько я понял, у вебкитовского движка есть встроенная оптимизация, и она насовсем отключается, если где-либо используются дочерние селекторы (включая +). Но эта оптимизация вообще странная какая-то, и как её результативность померять, неясно.
        • +1
          Не совсем, там идет работа с tree-traversal(сдвиг вправо по списку потомков) и tree-aware(узнать свое положение среди потомков) селекторами.
          Я сам не до конца в курсе деталей, но если по сути смотреть — селектор отрабатывает следующим образом: есть конечный узел, который берется из кэша (мапа id-шников, мапа классов, мапа элементов), дальше берется его CSS path и сверяется на матчинг. Соответственно, если в браузере используются еще и сложные пути селекторов, такой матчинг не отработает, и подключается сложная логика.
      • +1
        У флексбокса есть существенные недоработки на уровне концепции. При всех его больших достоинствах он частично разочаровывает на практике.
        • +1
          Можно ли узнать, чем он вас разочаровал?
          • 0
            вероятно, разнообразными глюками в работе во всех браузерах, которые лечатся исключительно адовыми костылями на js)
            Мне очень нравится флексбокс, но при всех его достоинствах — реализация во всех браузерах глючный кусок гуано, причем запах и цвет у всех разный напрочь. Я когда глубоко с ним работал (кастомные сложные лейауты, настраиваемые пользователем, через флексбокс) — впервые с 2012 года что ли написал хуки на юзерагент.
            • 0
              Странно, я заметил только глюки в хроме с местами некорректным рендерингом скроллбаров.
              • 0
                Пока вглубь не лезть — все хорошо.
                Да, именно. Скролл не пашет. А как вам, допустим, факт, что в этой верстке элемент не будет отображаться? Просто потому что процентная высота внутри элемента не отрабатывает. И это самый лайтовый из багов, он лечится тупо вотчдогом, который вручную проставляет width и height из clientWidth и clientHeight родителя.

                <div class="flex-grid">
                    <div class="flex-item">
                        <div id="victim" style="height: 100%">...</div>
                
                • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Почти уверен, что вы неправильно готовите.
                  Покажите пример на jsfiddle.
                  • 0
                    Да, немного криво объяснил. Берется % не от непосредственного родителя, а от флекс-элемента.

                    codepen.io/anon/pen/MYqaBP?editors=110

                    Смотрите, внутри первого .flexItem:
                    <header style="background: red;height: 50px"/>
                    <main style="background: green;height: calc(100% - 100px)"/>
                    <footer style="background: red;height: 50px"/>
                    


                    На calc можно не смотреть, это для примера подцеплено, без calc будет абсолютно идентичный глюк.
                    Футер — не виден, хотя должен быть виден: 50px + (100% — 100px) + 50px = 100%, по стандартам css — такие три блока должны помещаться внутри родителя.

                    Происходит это потому что 100% расчитывается не от элемента, а от родительского контейнера. Можете поинспектировать, если хотите.
                    Лечится вложенными контейнерами с ручным расчетом размеров по ресайзу.
                    • 0
                      Вы неправильно понимаете спецификацию codepen.io/anon/pen/dPqGRW
                      flex-direction действует только на непосредственных потомков.
                      Т.е. для достижения того, что вы хотите, нужно .flexItem также добавить flex-direction: column
                      • 0
                        Нет-нет. .flexItem — это не display: flex, это обычный display: block, я разве неверно понимаю спецификацию и так нельзя делать?
                        • 0
                          Можно, но вложив еще один див внутрь дива с display: block (как в вашем варианте) на него уже не действуют правила флексбокс, если можно так выразиться.
                          Еще раз, flex-direction устанавливает ось только для непосредственных потомков.
                          И я показал в своем варианте, как можно достичь того, что вы хотите.
                          • 0
                            За пример — спасибо, правда) Было слегка неожиданным для меня, что флексбоксы тянут друг друга так хорошо в этой ситуации.
          • 0
            Самое главное — нет никакого контроля над переносом блоков на следующую строку.

            Кроме того, очень не хватает режима распределения блоков внутри контейнера по центральным точкам, а не по межинтервалам (аналог функции distribute в ФШ).

            Ну и плюс глюки, как сказали выше. Причем, что интересно, больше всего в вебките. Сейчас примера под рукой нет, но там что-то было с высотой в 100%. И это был именно баг, поскольку я нашел его в багтрекере.

            У меня сложилось впечатление, что сейчас флекс это какое-то решение ни рыба ни мясо. В простых случаях можно обойтись и без него, а в сложных — он, решив одни проблемы, добавит новых.
          • +1
            Меня во флексе немного напрягает выравнивание последней строки при justify-content: space-between.

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

            В качестве решения я наталкивался на совет добавить к коллекции элементов недостающее количество и установить этим вспомогательным элементам height:0.

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

            Был бы признателен, если бы вы предложили вариант получше.
    • +2
      Кто же спорит) Я увидел статью эту, поискал её переводы на хабре, не нашёл и решил перевести. Тем лучше, что многие про это уже знают.
  • +9
    поселектать
    Записал в личный словарь
  • 0
    Вчера весь день верстал с применением этой техники, круто и снижает потребность в JS'е, но LESS файлы стали выглядеть страшно.
  • 0
    Как раз недавно встречал github.com/pascalduez/postcss-quantity-queries и не до конца тогда понял, что это и зачем. Сейчас, пока читал, подумал было, как бы это шаблонизировать похитрее на Sass/Stylus, и тут вспомнил про PostCSS. В postcss-quantity-queries есть :at-least, :at-most, :between и :exactly.

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