【less】今更始める less の基本文法と tips(2) - mixin 編

2018/12/01 追記

前書き

前回の続きです。

outofmem.hatenablog.com

mixin

less の特色はほとんどこの mixin に集約されていると言っても過言ではありません。

mixin の宣言

宣言と言うほどでもなく、既に定義したセレクタを呼び出せる機能です。

// less
.mixin {
  color: #000;
}

div {
  .mixin;
}
/* css */
.mixin {
  color: #000;
}
div {
  color: #000;
}

クラスではなく ID でも呼べますが、あんまりやらないっぽいですね。

// less
#mixin {
  color: #000;
}

div {
  #mixin;
}
/* css */
#mixin {
  color: #000;
}
div {
  color: #000;
}

名前空間

mixin はネストされた要素もすべて継承します。

// less
.mixin {
  span {
    color: #000;
  }
  .hoge {
    color: #fff;
  }
}

div {
  .mixin;
}
/* css */
div span {
  color: #000;
}
div .hoge {
  color: #fff;
}

ある特定の要素だけを mixin したい場合はセレクタ > セレクタとすることで可能です。

// less
.mixin {
  span {
    color: #000;
  }
  .hoge {
    color: #fff;
  }
}

div {
  .mixin > .hoge;
}
/* css */
div {
  color: #fff;
}

この機能を使うことで、mixin に名前空間ライクなものを付与することができます。

// less
// ベンダプレフィックスを吸収するためのmixin
#vendor {
  .border-radius {
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
  }
}

div {
  #vendor > .border-radius;
}
/* css */
div {
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-radius: 4px;
}

!importantの扱い

mixin 時に!importantをつけると mixin される全プロパティに!importantが付与されます。

// less
#vendor {
  .border-radius {
    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;
  }
}

div {
  #vendor > .border-radius !important;
}
/* css */
div {
  -moz-border-radius: 4px !important;
  -webkit-border-radius: 4px !important;
  border-radius: 4px !important;
}

引数つきの mixin

mixin には引数をつけることが可能です。

// less
#vendor {
  .border-radius(@param) {
    -moz-border-radius: @param;
    -webkit-border-radius: @param;
    border-radius: @param;
  }
}

div {
  #vendor > .border-radius(4px);
}
/* css */
div {
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-radius: 4px;
}

複数の引数を渡す

引数は,;で区切って宣言することができます。

less としては;を推奨しています。理由は、引数にデフォルト値を与えるときに,を使えるからです。(デフォルト値については後述)

// less
#vendor {
  .border-radius(@left-top; @right-top; @right-bottom; @left-bottom) {
    -moz-border-radius: @left-top @right-top @right-bottom @left-bottom;
    -webkit-border-radius: @left-top @right-top @right-bottom @left-bottom;
    border-radius: @left-top @right-top @right-bottom @left-bottom;
  }
}

div {
  // 引数を渡す時も";"が使える
  #vendor > .border-radius(4px; 2px; 3px; 5px);
}

しかしまぁ、上記の記述だとちょっと冗長すぎます。@argumentsと言う暗黙的に定義される変数を使うことでもうちょっと楽ができます。

// less
#vendor {
  .border-radius(@left-top; @right-top; @right-bottom; @left-bottom) {
    -moz-border-radius: @arguments;
    -webkit-border-radius: @arguments;
    border-radius: @arguments;
  }
}

div {
  #vendor > .border-radius(4px; 2px; 3px; 5px);
}
/* css */
div {
  -moz-border-radius: 4px 2px 3px 5px;
  -webkit-border-radius: 4px 2px 3px 5px;
  border-radius: 4px 2px 3px 5px;
}

ぶっちゃけてしまうと、,ないしは;が含まれていなければ一つの引数とみなすので、例のようにベンダプレフィックスを吸収するだけとかであれば、わざわざ複雑なことをせずにこれでいいと思います。

// less
#vendor {
  .border-radius(@param) {
    -moz-border-radius: @param;
    -webkit-border-radius: @param;
    border-radius: @param;
  }
}

div {
  // 半角スペースでわたす
  #vendor > .border-radius(4px 2px 3px 5px);
}
/* css */
div {
  -moz-border-radius: 4px 2px 3px 5px;
  -webkit-border-radius: 4px 2px 3px 5px;
  border-radius: 4px 2px 3px 5px;
}

ちょっと厄介なのが、パラメータとして,を含める可能性があるもの。

例えばbox-shadowですかね。下記のようにするとコンパイル時に「引数の数が一致する mixin が見つからないよ」と怒られてしまいます。

// less
#vendor {
  .box-shadow(@param) {
    -moz-box-shadow: @param;
    -webkit-box-shadow: @param;
    box-shadow: @param;
  }
}

div {
  // 複数の値を渡したいが…。
  #vendor > .box-shadow(0 0 4px #f00, 0 0 12px #000);
}

less はさりげなく可変長引数...をサポートしているのでこれで…!と思うのですが、それだと,が消滅してしまいます。

// less
#vendor {
  .box-shadow(@param...) {
    -moz-box-shadow: @param;
    -webkit-box-shadow: @param;
    box-shadow: @param;
  }
}

div {
  #vendor > .box-shadow(0 0 4px #f00, 0 0 12px #000);
}
/* css */
div {
  -moz-box-shadow: 0 0 4px #ff0000 /* ここの","が消滅 */ 0 0 12px #000000;
  -webkit-box-shadow: 0 0 4px #ff0000 0 0 12px #000000;
  box-shadow: 0 0 4px #ff0000 0 0 12px #000000;
}

じゃあどうするのかですが、~を使ってエスケープしてしまいましょう。

// less
#vendor {
  .box-shadow(@param) {
    -moz-box-shadow: @param;
    -webkit-box-shadow: @param;
    box-shadow: @param;
  }
}

div {
  #vendor > .box-shadow(~"0 0 4px #F00, 0 0 12px #000");
}
/* css */
div {
  -moz-box-shadow: 0 0 4px #f00, 0 0 12px #000;
  -webkit-box-shadow: 0 0 4px #f00, 0 0 12px #000;
  box-shadow: 0 0 4px #f00, 0 0 12px #000;
}

可変長引数

説明が前後しますが、less は...を使うことで可変長引数とすることができます。

引数が可変長引数のみの場合は識別子をつけなくても構いません。その場合は@argumentsで取得しましょう。

// less
.hoge(...) {
  border: @arguments;
}

.fuga(@color; @margin...) {
  color: @color;
  margin: @margin;
}

div {
  .hoge(solid, 10px, #00f);
  .fuga(#000, 0, 1px, 0, 0);
}
/* css */
div {
  border: solid 10px #0000ff;
  color: #000000;
  margin: 0 1px 0 0;
}

引数のデフォルト値

mixin の引数に@識別子: 値としておくことでデフォルト値を設定できます。

このデフォルト値は他の引数の値でも問題ありません。

// less
.hoge(@color: #000; @background-color: @color) {
  color: @color;
  background-color: @background-color;
}

div {
  .hoge();
}

span {
  .hoge(#fff);
}

table {
  .hoge(#fff, #f00);
}
/* css */
div {
  color: #000000;
  background-color: #000000;
}
span {
  color: #ffffff;
  background-color: #ffffff;
}
table {
  color: #ffffff;
  background-color: #ff0000;
}

名前付き引数

mixin を作っていると引数がめちゃめちゃ多くなったり、デフォルト値があったりなかったりする mixin を作成したくなることもあります。

そう言うときに便利なのが名前付き引数です。

// less
.hoge(@color: #000; @border; @background-color: @color) {
  // 第二引数にだけデフォルト値なし(こんなもん作らないようにしましょう)
  color: @color;
  border: @border;
  background-color: @background-color;
}

div {
  // 名前付き引数を使わない場合、
  // @colorもちゃんと引数で渡さなくてはならない
  .hoge(#000, solid 10px #00f);
}

span {
  // 名前付き引数を使っているので
  // @colorと@background-colorはちゃんとデフォルト値になる
  .hoge(@border: solid 10px #00f);
}

table {
  // 名前付き引数の場合は順番がむちゃくちゃでもOK
  .hoge(@background-color: #f00, @color: #fff, @border: solid 10px #00f);
}
/* css */
div {
  color: #000000;
  border: solid 10px #0000ff;
  background-color: #000000;
}
span {
  color: #000000;
  border: solid 10px #0000ff;
  background-color: #000000;
}
table {
  color: #ffffff;
  border: solid 10px #0000ff;
  background-color: #ff0000;
}

ルールセットを引数として渡す

CSS のルールそのものを引数として渡すことも可能です。@keyframesとかで便利です。

// less
#vendor {
  .keyframes(@name, @prop) {
    @-moz-keyframes @name {
      @prop();
    }
    @-webkit-keyframes @name {
      @prop();
    }
    @-o-keyframes @name {
      @prop();
    }
    @keyframes @name {
      @prop();
    }
  }

  .animation(@param) {
    -moz-animation: @param;
    -webkit-animation: @param;
    -o-animation: @param;
    -ms-animation: @param;
    animation: @param;
  }
}

div {
  #vendor > .keyframes(hoge, {0% {background-color: #fff;} 100%
        {background-color: #000}});

  #vendor > .animation(hoge 5s ease 0 infinite alternate);
}
/* css */
div {
  -moz-animation: hoge 5s ease 0 infinite alternate;
  -webkit-animation: hoge 5s ease 0 infinite alternate;
  -o-animation: hoge 5s ease 0 infinite alternate;
  -ms-animation: hoge 5s ease 0 infinite alternate;
  animation: hoge 5s ease 0 infinite alternate;
}
@-moz-keyframes hoge {
  0% {
    background-color: #fff;
  }
  100% {
    background-color: #000000;
  }
}
@-webkit-keyframes hoge {
  0% {
    background-color: #fff;
  }
  100% {
    background-color: #000000;
  }
}
@-o-keyframes hoge {
  0% {
    background-color: #fff;
  }
  100% {
    background-color: #000000;
  }
}
@keyframes hoge {
  0% {
    background-color: #fff;
  }
  100% {
    background-color: #000000;
  }
}

mixin と変数のスコープ

mixin 内で変数を宣言した場合、mixin を呼び出したスコープでその変数を使用することができます。

// less
.hoge {
  @base-color: #fff;
  color: @base-color;
}

div {
  .hoge();
  background-color: @base-color; // mixin内で宣言している変数を使用
}
/* css */
div {
  color: #ffffff;
  background-color: #ffffff;
}

mixin で呼び出した変数を書き換えてもそのスコープ内でしか値が変わりません。

また、変数を書き換えても mixin 内の値は変わりません。

// less
.hoge {
  @base-color: #fff;
  color: @base-color;
}

div {
  .hoge();
  @base-color: #000;
  background-color: @base-color;
}

span {
  .hoge();
  background-color: @base-color;
}
/* css */
div {
  color: #ffffff; /* @base-colorを書き換えたが、mixin内なので反映されない */
  background-color: #000000;
}
span {
  /* @base-colorを他スコープ(div)で書き換えられたが何も影響しない */
  color: #ffffff;
  background-color: #ffffff;
}

これを使うことで「mixin の返り値」を作成することができます。

// less
.average(@x; @y) {
  @average: ((@x + @y) / 2); // そう言えば演算関連まだ説明してないですね…。
}

div {
  .average(20px, 40px);
  margin: @average;
  padding: @average;
}
/* css */
div {
  margin: 30px;
  padding: 30px;
}

パターンマッチングと mixin のオーバーロード

同名かつ引数の数が違う mixin を作ることは可能なんですが、呼び出せる mixin はすべて呼び出すが基本ルールです。

// less
.hoge(@color) {
  color: @color;
}

.hoge(@margin) {
  margin: @margin;
}

div {
  .hoge(#fff); // @colorの方に行ってほしいが…
}
/* css */
div {
  color: #ffffff;
  margin: #ffffff; /* @marginの方も引数の数が一緒なので呼び出されてしまう */
}

上記の例を回避する方法はいくつかあります。まずはガードを使用し、引数の型をチェックする方法。

// less
.hoge(@color) when (iscolor(@color)) {
  color: @color;
}

.hoge(@margin) when (isnumber(@margin)) {
  margin: @margin;
}

div {
  .hoge(#fff);
}
/* css */
div {
  color: #ffffff;
}

キーワードを入れる方法もあります。

// less
.hoge(color, @color) {
  color: @color;
}

.hoge(margin, @margin) {
  margin: @margin;
}

div {
  .hoge(color, #fff);
}

span {
  .hoge(margin, 0);
}
/* css */
div {
  color: #ffffff;
}
span {
  margin: 0;
}

根本的な対処ではないですが、引数の数そのものが違ってもマッチングしないとみなしてくれます。

ただし、デフォルト値が設定されていると面倒なことになったりします。

// less
.hoge(@color) when (iscolor(@color)) {
  color: @color;
}

.hoge(@margin) when (isnumber(@margin)) {
  margin: @margin;
}

.hoge(@color, @border) {
  colol: @color;
  border: @border;
}

div {
  .hoge(#fff);
}

span {
  .hoge(#fff, solid 10px #0000ff);
}
/* css */
div {
  color: #ffffff;
}
span {
  color: #ffffff;
  border: solid 10px #0000ff;
}

ガード

ガードはかなり細かく設定できます。面倒ですが。

複数のガードを設定するときはandを使います。

// less
.hoge(@color, @margin) when (iscolor(@color)) and (isnumber(@margin)) {
  color: @color;
  margin: @margin;
}

div {
  .hoge(#fff, 10px);
}
/* css */
div {
  margin: 10px;
  color: #ffffff;
}

否定する場合はnotをつけます。

// less
.hoge(@color, @margin) when (iscolor(@color)) and (isnumber(@margin)) {
  color: @color;
  margin: @margin;
}

.hoge(@color, @border) when (iscolor(@color)) and not(isnumber(@border)) {
  color: @color;
  border: @border;
}

div {
  .hoge(#fff, 10px);
}

span {
  .hoge(#fff, solid 10px #0000ff);
}
/* css */
div {
  color: #ffffff;
  margin: 10px;
}
span {
  color: #ffffff;
  border: solid 10px #0000ff;
}

or 条件を指定したい場合は,を使います。

// less
.hoge(@margin, @padding: @margin) when (isnumber(@margin)), (isnumber(@padding)) {
  margin: @margin;
  padding: @padding;
}

div {
  .hoge(10px);
}

span {
  .hoge(20px, fuga); // @marginがnumberなのでコンパイルが通る
}
/* css */
div {
  margin: 10px;
  padding: 10px;
}
span {
  margin: 20px;
  padding: fuga;
}

また、数値であれば<>も使えますし、文字列やキーワードの判定なんかもできます。

// less
.hoge(@margin, @padding) when (@padding <= 0px) {
  @padding: 5px;
  margin: @margin;
  padding: @padding;
}

.hoge(@margin, @padding) when (@padding > 10px) {
  @padding: 10px;
  margin: @margin;
  padding: @padding;
}

div {
  .hoge(10px, -1px);
}

span {
  .hoge(20px, 20px);
}
/* css */
div {
  margin: 10px;
  padding: 5px;
}
span {
  margin: 20px;
  padding: 10px;
}
// less
// !=は用意されてないのでnotを使う
.hoge(@keyword, @color) when not(@keyword=bg) {
  color: @color;
}

.hoge(@keyword, @color) when (@keyword=bg) {
  background-color: @color;
}

div {
  .hoge(hoge, #fff);
}

span {
  .hoge(bg, #000);
}
/* css */
div {
  color: #ffffff;
}
span {
  background-color: #000000;
}

どこにもマッチングしなかった場合のフォールバックとして、default()を使うこともできます。

この辺を使いこなせるようになると本当にオーバーロードっぽくなるのでぜひマスターしましょう。

// less
.hoge(@margin, @padding) when (default()) {
  margin: @margin;
  padding: @padding;
}

.hoge(@margin, @padding) when (@padding <= 0px) {
  .hoge(@margin, 5px); // when (default())
}

.hoge(@margin, @padding) when (@padding > 10px) {
  .hoge(@margin, 10px); // when (default())
}

div {
  .hoge(10px, -1px); // when (@padding <= 0px)
}

span {
  .hoge(20px, 20px); // when (@padding > 10px)
}

table {
  .hoge(2px, 2px); // when (default())
}
/* css */
div {
  margin: 10px;
  padding: 5px;
}
span {
  margin: 20px;
  padding: 10px;
}
table {
  margin: 2px;
  padding: 2px;
}

まとめ

やはり長くなってしまった…。

次回はまだ説明してないあれこれを適当に説明していく予定です。