Немного flex и box

Все мы давно уже знаем, что как только наш любимый браузер Internet Explorer 9 выйдет из очередного круга мучительной поддержки — Flexbox наконец-то займет свою нишу.

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

Практика и краткие сведения:

Теория:

Справочники:

P.S: Содержимое некоторых ссылок на русском языке. Важно понимать, что данные про поддержку браузерами устарели. Подробнее об этом ниже.

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

Да, в IE10 поддерживается старый синтаксис, но для Autoprefixer это не проблема.

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

Соглашения

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

Разметка

Ах, как хотелось бы просто скопировать код из проекта, но нет — я использую HTML-препроцессор Jade, синтаксис которого основан на идентации и может быть не понятен читателю. А ещё highlight.js не имеет поддержку его синтаксиса. Придется копировать уже скомпилированную версию.

Замечание от 23.02.2016

Благодаря комментарию читателя исправлена проблема отображения кнопки переключения состояния меню в Firefox. Ранее кнопка в активном состоянии съезжала к центру блока navbar.

<!-- Компонент навигационного меню -->
<div class="navbar-component">
  <!-- Класс `area` — это простой контейнер (об этом позднее) -->
  <div class="navbar area">
    <!-- Логотип -->
    <a href="#" class="brand">Brand</a>
    <!-- Список ссылок -->
    <nav role="navigation" id="navigation" class="list">
      <a href="#" class="item -link">Home</a>
      <a href="#" class="item -link">Articles</a>
      <a href="#" class="item -link">Projects</a>
      <a href="#" class="item -link">Resources</a>
      <a href="#" class="item -link">About me</a>
      <span class="item">
        <i class="fa fa-search"></i>
      </span>
    </nav>
    <!-- Кнопка для мобильных -->
    <button data-collapse data-target="#navigation" class="toggle">
      <!-- Здесь будет иконка гамбургера -->
      <span class="icon"></span>
    </button>
  </div>
</div>

Скелет

Итак, первым делом нужно определить простейшие стили, которые все таскают из проекта в проект. Для экономии места я записал их в компактном 'режиме'.

// Scaffolding
*, *:before, *:after { box-sizing: border-box; }
body { background-color: #f5f5f5; color: #333; font-size: 14px; font-family: Verdana, Arial, sans-serif; line-height: 20px; }
a { text-decoration: none; transition: all 0.3s linear 0s; }

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

// Сетка
.area {
  // Объявляем этот блок flex-контейнером
  display: flex;
  // Центрируем
  margin-right: auto;
  margin-left: auto;
  // Устанавливаем главную ось и многострочность контейнера
  flex-flow: row wrap; // строка многострочный
  // Расположение элементов относительно поперечной оси на строке
  align-items: stretch; // растягиваются

  // Медиавыражения для типовых размеров экранов
  @media (min-width: 768px) { width: 750px; }
  @media (min-width: 992px) { width: 970px; }
  @media (min-width: 1200px) { width: 1140px; }
}

На этом с подготовкой фундамента мы закончим. Теперь определим визуальные стили для разрабатываемого нами компонента навигационного меню. Главный селектор компонента будет иметь такой вид:

// Component
.navbar-component {
  background-color: @navbar-background; // 1
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.16), 0 2px 10px rgba(0, 0, 0, 0.12); // 2

  // Намекаем сетке, что
  & > .navbar {
    // Выравнивание относительно главной оси
    justify-content: space-between; // равномерное
  }
}

Замечания

1. Разумеется, что все переменные инициализированы ранее, просто я не указываю это в статье, ибо смысла в этом нет.

2. Тени генерирует библиотека material shadows, которую можно установить, используя менеджер пакетов Bower: bower i --save-dev material-shadows.

Давайте отвлечемся от строк сурового кода и посмотрим на то, что делает свойство justify-content. Это изображение показывает то, что было:

До применения justify-content

А теперь посмотрите на то, что стало:

После применения justify-conten

Неплохо, да? Если бы мы не использовали Flexbox, пришлось бы добавлять свойство float, как минимум селекторам brand, toggle, list и item. Причем не просто добавлять свойства, а следить за тем, чтобы они нормально отображались в различных условиях.

Стилизация основных элементов

Приступим к базовой (визуальной) стилизации основных элементов нашего компонента.

Список пунктов меню

Сначала необходимо определить базовые стили для пунктов меню:

// Component
.navbar {
  // List of items
  & > .list {
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
  }

  & > .list > .item {
    display: block;

    // Возможность элемента сжиматься
    flex-shrink: 0; // запрещено

    // Выравнивание и отступы
    height: @navbar-height;
    line-height: @navbar-height;
    padding-left: round((@navbar-height - 20) / 2);
    padding-right: round((@navbar-height - 20) / 2);

    text-transform: uppercase;
    color: @navbar-item-color;
    font-size: @navbar-item-font-size;
  }

  & > .list > .item.-link {
    line-height: (@navbar-height + @navbar-item-border-width);
    color: @navbar-item-color;
    border-bottom: @navbar-item-border-width solid @navbar-item-border;

    &.-active,
    &:hover,
    &:focus {
      color: @navbar-item-active-color;
      border-bottom-color: @navbar-item-active-border;
    }
  }
}

Следует немного пояснить эту простыню кода перед тем, как идти дальше к другим элементам нашего компонента.

Во-первых, мы указываем селектору list стать flex-контейнером, у которого главная ось является горизонтальной и однострочной, а также элементы располагаются по центру поперечной оси.

Во-вторых, пункты меню могут быть не только ссылками, но и, допустим, элементом <span>, который включает в себя иконки и прочие шалости. Для этого мы отдельно определяем ключевые свойства пункта меню (выравнивание, отступы, трансформацию текста и его цвет). Кстати, здесь выравнивание элемента осуществляется с помощью свойства line-height, так как у элемента, если он ссылка, может быть нижняя рамка, а у обычных элементов её нет. То есть нижнюю рамку мы учитываем только у ссылок.

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

Переключатель состояния

Для удобства скроем список пунктов меню, изменив значение его свойства dispaly на none.

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

// Component
.navbar {
  // Toggle button
  & > .toggle {
    border: 0;
    background-color: transparent;
    display: inline-block;
    outline: none;
    border: 0;
    background-color: transparent;
    background-image: none;
    vertical-align: middle;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    touch-action: manipulation;
    user-select: none;

    // Напомню, что 20 — это значение line-height у body
    padding: round((@navbar-height - 20) / 2);
  }

  // List of items
  & > .list { ... }
}

Займемся иконкой гамбургера.

Я человек ленивый и каждый раз, когда делается что-то тривиальное и рутинное — у меня сразу же возникает мысль вынести это в модуль, чтобы избавиться от копирования стилей из проекта в проект. В нашем случае модулем может являться less-файл, содержащий какие-то примеси. Так родилась небольшая библиотека hamburger-icon, которая просто генерирует иконку гамбургера и позволяет её анимировать.

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

// Component
.navbar {
  // Toggle button
  & > .toggle { ... }

  & > .toggle > .icon {
    // В силу особенной препроцессора Less, определяем настройки библиотеки
    .hamburger-settings(24px, 3px, 5px, @navbar-item-color, @timing-function: linear);

    // Генерируем иконку
    .hamburger-generator();
  }

  // Определяем активное состояние для гамбургера
  & > .toggle.-active > .icon {
    .hamburger-animation();
  }

  // List of items
  & > .list { ... }
}

В силу того, что подключить внешний ресурс для Less на Codepen не получится, там будет находиться вручную перенесенная версия иконки. И, к сожалению, для полноты картины я должен буду вставить её сюда с комментариями:

& > .toggle > .icon {
  position: relative;
  margin-top: 8px;
  margin-bottom: 8px;

  // Создаем три палки
  &,
  &:before,
  &:after {
    display: block;
    width: 24px;
    height: 3px;
    transition: background-color 0.3s linear, transform 0.3s linear;
    background-color: #555555;
  }

  // Абсолютно позиционируем палки и раздвигаем их по вертикали
  &:before, &:after { position: absolute; content: ""; }
  &:before { top: -8px; }
  &:after { top: 8px; }
}

& > .toggle.-active > .icon {
  // Скрываем среднюю палку
  background-color: transparent;

  // Поворачиваем верхнюю и нижнюю палки
  &:before { transform: translateY(8px) rotate(45deg); }
  &:after { transform: translateY(-8px) rotate(-45deg); }
}

Отлично, теперь можно убрать с глаз долой эту кнопку и заняться логотипом. Пока что просто изменим значение свойства display у селектора toggle на none. К этому мы ещё вернемся.

Логотип

В этой части статьи всё просто. Нужно лишь понимать, что логотип может быть картинкой или же, в бюджетной версии, — текстом.

Сразу оговорюсь, что вариант с картинкой рассматриваться не будет, так как там нужно всего лишь указать изображение в background-image и скорректировать значение свойства margin по своим размерам.

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

// Component
.navbar {
  // Brand
  & > .brand {
    display: block;
    font-size: 16px;
    color: #777;
    margin: round((@navbar-height - 20) / 2);
  }

  // Toggle button
  & > .toggle { ... }

  // List of items
  & > .list { ... }
}

На этом базовая стилизация заканчивается.

Корректировка поведения элементов

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

  • Логотип должен отображаться всегда.
  • Кнопка переключения состояния списка пунктов меню должна быть видна только на устройствах с небольшим экраном.
  • Список пунктов меню должен скрываться на маленьких устройствах и всегда отображаться на больших.

Здесь нам пригодится принцип сначала мобильные (Mobile First).

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

На очереди кнопка переключения состояния меню. Изначально она должна отображаться, а начиная с какого-то значения ширины экрана — скрываться. Таков принцип сначала мобильные. Сказано — сделано:

// Component
.navbar {
  // Brand
  & > .brand { ... }

  // Toggle button
  & > .toggle {
    ...

    @media (min-width: @navbar-collapse-breakpoint) {
      display: none;
    }
  }

  // List of items
  & > .list { ... }
}

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

// Component
.navbar {
  // Brand
  & > .brand { ... }

  // Toggle button
  & > .toggle { ... }

  // List of items
  & > .list {
    ...
    display: none;
    
    @media (min-width: @navbar-collapse-breakpoint) {
      display: flex;
    }

    &.-on {
      display: flex;
    }
  }
}

Класс -on определяет открытое состояние списка пунктов меню в том случае, если пользователь нажал на созданную нами кнопку. Кстати, вот листинг JavaScript-кода, который отображает и скрывает меню при клике:

(function() {

  // Определение вызывающего элемента
  var getTriggerElement = function(el) {
    // Получаем атрибут `data-collapse`
    var isCollapse = el.getAttribute('data-collapse');
    // Если атрибут существует, то
    if (isCollapse !== null) {
      // Возвращаем элемент на котором осуществлен клик
      return el;
    } else {
      // Иначе пытаемся найти атрибут у его родителя
      var isParentCollapse = el.parentNode.getAttribute('data-collapse');
      // Возвращаем родительский элемент или undefined
      return (isParentCollapse !== null) ? el.parentNode : undefined;
    }
  };

  // Обработчик клика
  var collapseClickHandler = function(event) {
    // Определение вызывающего элемента
    var triggerEl = getTriggerElement(event.target);
    // Если у элемента и его родителя нет атрибута
    if (triggerEl === undefined) {
      // Отменяем действие
      return false;
    } else {
      event.preventDefault();
    }

    // Получаем целевой элемент
    var targetEl = document.querySelector(triggerEl.getAttribute('data-target'));
    // Если целевой элемент существует
    if (targetEl) {
      // Манипулируем классами
      triggerEl.classList.toggle('-active');
      targetEl.classList.toggle('-on');
    }
  };

  // Делегируем событие
  document.addEventListener('click', collapseClickHandler, false);

})(document, window);

Осталось ещё немного.

Прокрутка списка

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

Немного подумав, можно прийти к следующему сценарию в коде:

Первым делом, необходимо запретить перенос строк:

// List of items
.navbar {
  & > .list {
    ...
    white-space: nowrap;
  }
}

Далее нужно создать медиавыражение, стили внутри которого будут действовать только до указанной точки:

// List of items
.navbar {
  & > .list {
    ...
    @media (max-width: @navbar-collapse-breakpoint) {
      position: fixed; // 1
      top: @navbar-height; // 1
      left: 0; // 1
      width: 100%;
      overflow-y: hidden;
      overflow-x: auto;
      border-top: 1px solid @navbar-border;
      background-color: @navbar-background; // 1
    }
  }
}

Немного подробнее об этом медиавыражении:

  • Свойство position: fixed позволяет абсолютно позиционировать наше меню, чтобы не «отодвигать» блоки ниже него.
  • Свойство overflow-y: hidden скрывает все лишнее снаружи, а свойство overflow-x: auto позволяет браузеру при желании отображать ползунок скролла.
  • Так как у нас абсолютное позиционирование, то необходимо указать цвет фона этого блока.

Замечания

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

Демонстрация

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

Создание Flexbox навигационного меню

Разумеется, рабочий пример можно посмотреть и покликать на CodePen.

Надеюсь, вам понравилось.

Выводы

Это круто! Действительно, Flexbox — это то, чего раньше не хватало для простого и логичного управления блоками макета. Хотите сменить ориентацию блока относительно оси Y или X? — всего одно свойство и все готово. Хотите максимально равномерно распределять блоки по доступной ширине? — снова одно свойство и все готово.

Да, Flexbox хорош, но имеет одно очень весомое обстоятельство в пользу того, чтобы от него временно отказаться — поддержка браузерами. Вернее сказать, поддержка одного браузера, который до сих пор любят заказчики. Если же ваши заказчики не живут в мире «прошлом», то Flexbox, пожалуй, хорошая помощь верстальщику.

Зато в приложениях, написанных на NW.js его можно использовать сколько душе угодно.