Отображение треугольника на CSS при помощи SASS-миксина

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

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

Должен сказать, что я так и не смог запомнить этот маленький кусочек кода. Какие рамки должны быть прозрачными? Какие – сплошными? Я не могу это сообразить «на лету», да и вы наверняка тоже. Так что создание «фальшивых» треугольников – типичная задача для автоматизации при помощи SASS.

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

Что нам понадобится

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

Во-первых, направление. Мы должны указать одно направление из четырёх: top (вверх), right (вправо), bottom (вниз) или left (влево).

Во-вторых, позиция. Например, 1,5 em от верхнего края или 100% от левого. Также полезно было бы иметь возможность указать цвет и размер, хотя эти параметры должны иметь значения по умолчанию.

Короче говоря, наша примесь – это способ сказать браузеру на языке CSS: «Сделай треугольник, указывающий в таком-то направлении на такую-то точку, такого-то цвета и таких-то размеров». Звучит внушительно.

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

.element {
  /* Container of some kind */
 
  &::before {
    /* Including triangle mixin */
  }
}

Наши инструменты

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

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

Например, мы можем использовать примесь size(), чтобы объединить два параметра: ширину и высоту. Ещё нам пригодится примесь absolute() для позиционирования.

Для удобства я перенесу повторно используемый нами код в эту статью. Теперь все 3 примеси у нас как на ладони:

// Sizing stuff
@mixin size($width, $height: $width) {
      width: $width;
      height: $height;
}
 
// Positioning stuff
@mixin position($position, $args) {
  @each $o in top right bottom left {
        $i: index($args, $o);
 
    @if $i and $i + 1 <;= length($args) and type-of(nth($args, $i + 1)) == number  {
          #{$o}: nth($args, $i + 1);
    }
  }
 
  position: $position;
}
 
// Absolutely positioning stuff
@mixin absolute($args) {
        @include position(absolute, $args);
}

Наконец, нам понадобится функция opposite-direction(): из этой статьи в Compass либо из моей прошлой статьи.

Строим примесь

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

@mixin triangle(
  $direction,
      $position,
  $color: currentColor, 
      $size: 1em
) {
  /* Mixin content */
}

Направление (direction) и позиция (position) являются обязательными параметрами. Цвет же задавать не обязательно: по умолчанию он принимает значение текущего цвета, заданного в нашем CSS-стиле (currentColor).

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

Но ничто не мешает вам сделать этот параметр обязательным.

Для любителей формального синтаксиса деклараций всё может выглядеть вот так:

triangle(string $direction, list $position, color $color: currentColor, number $size: 1em)

Ядро кода

Начнём с простого. Ядро примеси задаёт размерность и позицию треугольника. О границах позаботимся потом:

@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
  @include absolute($position);
  @include size(0);
  content: '';
  z-index: 2;
 
  /* Border stuff */
}

Всё предельно понятно, не правда ли? Вот, что делает наше ядро:

  1. Позиционирует элемент при помощи примеси absolute();
  2. Задаёт размер 0 пикселей в ширину, 0 в высоту;
  3. В случае, когда мы имеем дело с псевдо элементом (а это будет почти всегда), отображаем его при помощи свойства content;
  4. Используем z-index, чтобы удостовериться в том, что элемент не перекрывается другими (вы можете этого не делать).

Границы

До сих пор всё было просто. В основном благодаря тому, что у нас уже были готовые примеси, облегчающие жизнь. Теперь займёмся границами элемента. Пора ввести в игру opposite-direction(). Я напомню вам действие этой функции на таком примере.

Допустим, мы хотим, чтобы треугольник указывал направо. Тогда:

  1. Правая граница должна остаться не определённой;
  2. Левая граница должна быть заполненной;
  3. Верхняя граница должна быть прозрачной;
  4. Нижняя граница должна быть прозрачной.

Это можно выразить следующим кодом:

@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
  /* Core stuff */
 
  border-#{opposite-position($direction)}: $size * 1.5 solid $color;
      $perpendicular-border: $size solid transparent;
 
  @if $direction == top or $direction == bottom {
    border-left:   $perpendicular-border;
        border-right:  $perpendicular-border;
  }
 
  @else if $direction == right or $direction == left {
    border-bottom: $perpendicular-border;
        border-top:    $perpendicular-border;
  }
}

Первым делом мы определили противоположную границу при помощи функции opposite-direction(). В нашем предыдущем примере opposite-direction возвращала значение «left», так что CSS-свойство будет «border-left».

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

Затем мы определили границы, перпендикулярные направлению треугольника. Если заданы направления top или bottom, то перпендикулярными границами являются border-right и border-left, и наоборот. Обе перпендикулярные границы должны быть прозрачными.

Обработка ошибок

Как и везде, нельзя забывать об обработке неправильных параметров в нашей примеси. Сначала надо убедиться, что с направлением ($direction) всё нормально.

Иными словами, этот параметр надо проверить на равенство одному из четырёх предопределённых значений. Я считаю, что проверить этот параметр полезно, так как многие захотят передать значение в градусах, как в функцию linear-gradient(), например, «24deg»:

@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
  @if not index(top right bottom left, $direction) {
    @warn "Direction must be one of `top`, `right`, `bottom` or `left`; currently `#{$direction}`.";
  }
 
  @else {
    /* Mixin content */
  }
}

Обратите внимание, что для валидации направления мы используем функцию index(), а не последовательный перебор с проверкой на равенство.

В SASS 3.3 мы можем сделать функцию ещё более устойчивой к некорректному вводу, приведя входные параметры к нижнему регистру: $direction: to-lower-case($direction). Это позволит принимать параметры в верхнем регистре. Подробности важны.

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

А пока это будет выглядеть так:

@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
  $direction: if(function-exists("to-lower-case") == true, to-lower-case($direction), $direction);
 
  @if not index(top right bottom left, $direction) {
    @warn "Direction must be one of `top`, `right`, `bottom` or `left`; currently `#{$direction}`.";
  }
 
  @else {
    /* Mixin content */
  }
}

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

Пример использования

Использовать примесь triangle() довольно просто, но нужно иметь ввиду, что, поскольку треугольник использует абсолютное позиционирование, его родитель должен иметь свойство «position: relative»:

/**
 * 1. Enable absolute positioning for pseudo-element
 * 2. Using a pseudo-element to generate the arrow
 * 3. Same as @include triangle(bottom,top 100% left 1em, $color);
     */
    .tooltip { 
      $color: #3498db;
 
  position: relative; /* 1 */
 
  background: $color;
  padding: .5em;
  border-radius: .15em;
  color: white;
  text-align: center;
 
  &::before { /* 2 */
    @include triangle( 
      $direction : bottom, 
          $position  : top 100% left 1em, 
      $color     : $color
    ); /* 3 */
  }
}

Также вы можете поиграть с этим примером онлайн.

Заключительные мысли

В конце концов, наша примесь оказалась довольно простой. Так получилось не только потому, что мы использовали написанный ранее код, но и потому, что мы применили продвинутые техники вроде opposite-direction() или @if, чтобы как можно меньше повторяться в соответствии с принципом DRY.

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

Перевод статьи «A Sass Mixin for CSS Triangles» был подготовлен дружной командой проекта Сайтостроение от А до Я.