CSS
Sass
scss

Sassで@mixinを作る時に知っておきたい基礎知識

More than 1 year has passed since last update.

はじめに

Sassの@mixinはメンテナブルなCSS設計にも役立つ強力な機能です。
一方でゆるくも書けてしまうため、気をつけないと不格好なCSSが簡単に出力されてしまいます。

そこで@mixinを自作するにあたって、知っておきたい(おきたかった)基本知識をまとめました。何かの参考にでもなれば幸いです。

前提条件

  • ソースコードは.scss形式で記述しています
  • Sass 3.4.4で動作を確認しています
  • 使用しているModifierという単語は以下の通りです

BEMと呼ばれる命名規則で使われる、基本的なスタイル(セレクタ・class)に追加する形で装飾を調整するclassのこと

目次

  • @mixinのおさらい
    • 基本的な使い方
    • @contentの使い方
  • Modifierに無駄なスタイルを適用させたくない...
    • @contentを使ってみる
    • 制御用の引数を用意してみる
    • 引数に配列(リスト)を使ってみる
    • 引数に連想配列を使ってみる
  • 番外編: 連想配列を入れ子にする
  • 無駄なセレクタを増やしたくない...
    • @at-rootを使ってみる
    • &を使ってみる
  • おまけ: @debugを使う
  • 終わりに

@mixinのおさらい

基本的な使い方

  • 引数によって細かくスタイルの設定ができます
  • , 区切りで複数の引数を渡すことができます
  • $arg: val という形で引数の初期値の設定ができます
mixinの基本的な使い方.scss
@mixin hoge($color: #fff, $fSize: 16px) {
  color: $color;
  font-size: $fSize;
}
.text {
  @include hoge(black, 20px);
}
.text2 {
  // 引数は初期値のを使用
  @include hoge();
}
mixinの基本的な使い方.css
.text {
  color: black;
  font-size: 20px;
}
.text2 {
  color: #fff;
  font-size: 16px;
}

値に,が含まれるプロパティには()で囲んで呼び出すか、引数に...をつけます

可変長引数.scss
@mixin hoge($family) {
  font-family: $family;
}
.text {
  @include hoge((Arial, Helvetica, sans-serif));
}

// もしくは

@mixin hoge($family...) {
  font-family: $family;
}
.text {
  @include hoge(Arial, Helvetica, sans-serif);
}
可変長引数.css
.text {
  font-family: Arial, Helvetica, sans-serif;
}

引数名をつけて呼び出すことで、順序を気にしないで設定・好きなものだけ選択できます

引数名を指定する.scss
@mixin hoge($color: #fff, $fSize: 16px) {
  color: $color;
  font-size: $fSize;
}
.text {
  @include hoge($fSize: 20px, $color: black);
  // ↓だとcolor: 20px; font-size: black;
  // @include hoge(20px, black);
}

引数には配列(リスト)や連想配列(マップ・オブジェクト)を渡すこともできます。※後述

@contentの使い方

  • @mixinの中で@content;と記述するとコンテントブロックを渡すことができます
  • これにより1つのmixinで呼び出し側に合わせた処理が可能です
contentの使い方.scss
@mixin hoge() {
  // js有効時には非表示にしたい
  .js & {
    display: none;
    @content;
  }
}
.div {
  @include hoge();
}
.div2 {
  @include hoge() {
    // この中が@mixinの@contentに追加
    font-weight: bold;
  }
}
contentの使い方.css
.js .div {
  display: none;
}
.js .div2 {
  display: none;
  font-weight: bold;
}

Modifierに無駄なスタイルを適用させたくない...

本題(?)です。
例としてModifierに応じて文字色が変わるようなCSSを@mixinで生成したいとします。

動作の説明しやすさを優先したため、これだとあまりmixinを使う意味は...というのは見逃してください x(

何も意識せずにそのまま書くと....scss
@mixin hoge($color: #fff, $fSize: 16px) {
  color: $color;
  font-size: $fSize;
  font-weight: bold;
  &:hover {
    text-decoration: none;
  }
}
.text {
  @include hoge(black);
  &.is-red {
    @include hoge(red);
  }
  &.is-blue {
    @include hoge(blue);
  }
}

問題なく動作します。
が、当然CSSは以下のように出力されます。

何も意識せずにそのまま書くと....css
.text {
  color: black;
  font-size: 16px;
  font-weight: bold;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
  font-size: 16px;
  font-weight: bold;
}
.text.is-red:hover {
  text-decoration: none;
}
.text.is-blue {
  color: blue;
  font-size: 16px;
  font-weight: bold;
}
.text.is-blue:hover {
  text-decoration: none;
}

Modifierは色だけ変えたいのですが、それ以外のスタイルがblockと同じように設定されてしまいました。

@contentを使ってみる

@contentを使ってModifierを制御してみます。

contentを使ってみる.scss
@mixin hoge($color: #fff) {
  @content;
  color: $color;
}
.text {
  @include hoge(black) {
    font-size: 16px;
    font-weight: bold;
    &:hover {
      text-decoration: none;
    }
  }
  &.is-red {
    @include hoge(red);
  }
  &.is-blue {
    @include hoge(blue);
  }
}
contentを使ってみる.scss
.text {
  font-size: 16px;
  font-weight: bold;
  color: black;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
}
.text.is-blue {
  color: blue;
}

cssはキレイにはなりましたが、結局共通スタイルを@content;で渡している(セレクタごとに直書きする必要がある)ため、mixinを使う意味が少なくなってしまいました。

制御用の引数を用意してみる

制御用の引数を用意し、引数の値をもとにModifierを制御してみます。

制御用の引数を用意してみる.scss
@mixin hoge($color: #fff, $fSize: 16px, $isBlock: false) {
  // Modifierじゃなかったら
  @if ($isBlock) {
    font-size: $fSize;
    font-weight: bold;
    &:hover {
      text-decoration: none;
    }
  }
  color: $color;
}
.text {
  @include hoge($color: black, $isBlock: true);
  &.is-red {
    @include hoge(red);
  }
  &.is-blue {
    @include hoge(blue);
  }
}
制御用の引数を用意してみる.css
.text {
  color: black;
  font-size: 16px;
  font-weight: bold;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
}
.text.is-blue {
  color: blue;
}

いい感じに出力されました!
Modifierとして付与したいスタイルのみ設定されていることが分かります。
簡単なものであればこれでも良さそうです。

引数に配列(リスト)を使ってみる

少し考え方を変えて、配列を使ってModifierを調整してみます。

  • Modifierとして使用するClassと設定する色をそれぞれ配列にします
  • Classと色の順番は合わせます
配列を使ってみる.scss
@mixin hoge($baseColor: #fff, $fSize: 16px, $selectors:(), $colors:()) {
  color: $baseColor;
  font-size: $fSize;
  font-weight: bold;
  &:hover {
    text-decoration: none;
  }

  // 引数の数を取得
  $len: length($selectors);

  @for $i from 1 through $len {
    &#{nth($selectors, $i)} {
      color: nth($colors, $i);
    }
  }
}
.text {
  @include hoge(
    $baseColor: black,
    $selectors: ('.is-red', '.is-blue'),
    $colors: (red, blue)
  );
}
配列を使ってみる.css
.text {
  color: black;
  font-size: 16px;
  font-weight: bold;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
}
.text.is-blue {
  color: blue;
}

いい感じに出力されました!
ただし、class名と色の順番を合わせるのが面倒かもしれません。

引数に連想配列を使ってみる

配列として渡していた2つのリストを key + value としてまとめ、連想配列として渡します

連想配列を使ってみる.scss
@mixin hoge($baseColor: #fff, $fSize: 16px, $data:()) {
  font-size: $fSize;
  font-weight: bold;
  color: $baseColor;
  &:hover {
    text-decoration: none;
  }
  @each $key, $val in $data {
    &#{$key} {
      color: $val;
    }
  }
}
.text {
  $data: (
    '.is-red'  : red,
    '.is-blue' : blue,
    '.is-green': green
  );
  @include hoge($baseColor: black, $data: $data);
}
連想配列を使ってみる.css
.text {
  font-size: 16px;
  font-weight: bold;
  color: black;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
}
.text.is-blue {
  color: blue;
}
.text.is-green {
  color: green;
}

いい感じに出力されました!
配列を使っていた時と比べて、スマートになった気がします。

番外編: 連想配列を入れ子にする

連想配列の値には更に配列を指定することもできるので、入れ子にしてより複雑なことも可能です。

複雑なデータを元にした.scss
@mixin hoge($baseColor: #fff, $fSize: 16px, $data:()) {
  font-size: $fSize;
  font-weight: bold;
  color: $baseColor;
  &:hover {
    text-decoration: none;
  }

  @each $selector, $style in $data {
    &#{$selector} {
      @each $prop, $val in $style {
        #{$prop}: #{$val};
      }
    }
  }
}
.text {
  $data: (
    '.is-red': (
      color: red,
      background-color: white
    ),
    '.is-blue': (
      color: blue,
      background-color: green
    ),
    '.is-green': (
      color: green,
      background-color: blue
    )
  );
  @include hoge($baseColor: black, $data: $data);
}
複雑なデータを元にした.css
.text {
  font-size: 16px;
  font-weight: bold;
  color: black;
}
.text:hover {
  text-decoration: none;
}
.text.is-red {
  color: red;
  background-color: white;
}
.text.is-blue {
  color: blue;
  background-color: green;
}
.text.is-green {
  color: green;
  background-color: blue;
}

呼び出し側で定義されるものが増えましたが、だいぶ汎用的になりました。

  • mixinとしての汎用性と粒度のバランスが難しいかもしれません
  • 場合によっては@content;を使った方がスマートかもしれません

無駄なセレクタを増やしたくない...

BEMのような命名規則でそのままElementを指定したり、シングルクラス式で書いたりすると無駄なセレクタが増えてしまうことがあります。

1つのmixin内にElementを用意した.scss
@mixin hoge($fSize: 16px) {
  font-size: $fSize;
  font-weight: bold;
  &:hover {
    text-decoration: none;
  }
  // ElementとそのModifierをmixin内に用意
  // #{&} は呼び出すBlock名(ここでは'nagaiClassNameDayo')が入る
  #{&}__head {
    margin: 10px;
    &.is-red {
      color: red;
    }
  }
  #{&}__body {
    padding: 0;
    &.is-blue {
      color: blue;
    }
  }
  // BlockのModifierをシングルクラス式で用意
  #{&}--hide {
    display: none;
  }
}
.nagaiClassNameDayo {
  @include hoge();
}
1つのmixin内にElementを用意した.css
.nagaiClassNameDayo {
  font-size: 16px;
  font-weight: bold;
}
.nagaiClassNameDayo:hover {
  text-decoration: none;
}
.nagaiClassNameDayo .nagaiClassNameDayo__head {
  margin: 10px;
}
.nagaiClassNameDayo .nagaiClassNameDayo__head.is-red {
  color: red;
}
.nagaiClassNameDayo .nagaiClassNameDayo__body {
  padding: 0;
}
.nagaiClassNameDayo .nagaiClassNameDayo__body.is-blue {
  color: blue;
}
.nagaiClassNameDayo .nagaiClassNameDayo--hide {
  display: none;
}

.nagaiClassNameDayoというclass名が全てに出現してしまいました。
命名規則でclass名の衝突を避けているはずなのでこれは無駄かもしれません。

@at-root を使ってみる

@at-rootとセレクタの前に記述すると、そのセレクタのネスト状態を解除して出力してくれます

@at-rootを追加した.scss
@mixin hoge($fSize: 16px) {
  font-size: $fSize;
  font-weight: bold;
  &:hover {
    text-decoration: none;
  }
  // @at-rootを宣言
  @at-root #{&}__head {
    margin: 10px;
    &.is-red {
      color: red;
    }
  }
  // @at-rootを宣言
  @at-root #{&}__body {
    padding: 0;
    &.is-blue {
      color: blue;
    }
  }
  // @at-rootを宣言
  @at-root #{&}--hide {
    display: none;
  }
}
.nagaiClassNameDayo {
  @include hoge();
}
@at-rootを追加した.css
.nagaiClassNameDayo {
  font-size: 16px;
  font-weight: bold;
}
.nagaiClassNameDayo:hover {
  text-decoration: none;
}
.nagaiClassNameDayo__head {
  margin: 10px;
}
.nagaiClassNameDayo__head.is-red {
  color: red;
}
.nagaiClassNameDayo__body {
  padding: 0;
}
.nagaiClassNameDayo__body.is-blue {
  color: blue;
}
.nagaiClassNameDayo--hide {
  display: none;
}

いい感じに出力されました!
#{&}で親のclass名を保持しつつ、@at-rootを宣言したことでネストが解除されていることが分かります。
なお@at-rootはmixin内でなくても使用することができます。

mixinでなくても書ける.scss
.text {
  display: block;
  @at-root #{&}__head {
    display: block;
  }
  @at-root #{&}__body {
    display: block;
  }
}
mixinでなくても書ける.css
.text { display: block;}
.text__head { display: block;}
.text__body { display: block;}

※使い方によってはスタイルを破壊してしまうのでご注意ください!

こんなことをしてしまうと....scss
.text {
  @at-root * {
    display: none;
  }
}

&を使ってみる

sass 3.3より #{&} だけでなく & という書き方もできるようになりました。
&を使うとSass側がElementやModifierと見なしてくれるのか、自動的にネストを解除して出力してくれます

&に変更した.scss
@mixin hoge($fSize: 16px) {
  font-size: $fSize;
  font-weight: bold;
  &:hover {
    text-decoration: none;
  }
  // #{&} -> & へ
  &__head {
    margin: 10px;
    &.is-red {
      color: red;
    }
  }
  &__body {
    padding: 0;
    &.is-blue {
      color: blue;
    }
  }
  &--hide {
    display: none;
  }
}
.nagaiClassNameDayo {
  @include hoge();
}

@at-root を使った時と同じように出力されます(コンパイル後のCSSは省略)。
今までのように#{&}を使うとclass名が崩れるケースもあるため、&を使う場合は&をそのまま使った方が良いかもしれません。

参考
http://redline.hippy.jp/restart/sass/post-436.php

おまけ: @debugを使う

mixinで思うように出力されない!そんな時は@debugが役に立つかもしれません。
JavaScriptでのconsole.log()のようなもので、@debug XXX; と記述するとその位置でのXXXの内容がコマンドラインに出力されます。

例えば以下のようなmixinを使うと.scss
@mixin hoge() {
  &__inner {
    display: block;
  }
  @at-root #{&}__inner2 {
    display: block;
  }
}
.hoge, .fuga {
  @include hoge();
}
例えば以下のようなmixinを使うと.css
.hoge__inner, .fuga__inner {
  display: block;
}
.hoge, .fuga__inner2 {
  display: block;
}

@at-root #{&} で指定した方は思うようにコンパイルしてくれません。
何が起こっているのか@debugで確認してみます。

何が起こっているのか確認するためにdebug仕込む.scss
@mixin hoge() {
  @debug &;
  @debug #{&};
  @debug type-of(&);  // 型を返す
  @debug type-of(#{&}); // 型を返す
  // 略...
}
xxx.scss:4 DEBUG: .hoge, .fuga
xxx.scss:5 DEBUG: .hoge, .fuga
xxx.scss:6 DEBUG: list
xxx.scss:7 DEBUG: string

一見同じ値が渡されていますが、実はデータタイプが異なっています。
つまり上記では ".hoge, .fuga" という文字列がそのまま適用されてコンパイルされたと考えられます。「#{&}を使うとclass名が崩れるケース」とはこのようなことですね。

終わりに

CSSプリプロセッサはWebデザイナーやコーダーといったノンプログラマーでも、触る機会は多いかと思います。
とりあえずで、色んなパターンのCSSが出力できるため、出力されたCSS自体を意識することは少ないかもしれません。
ですが、無駄なスタイルはパフォーマンス低下に繋がるため、ノンプログラマーでもなるべく気をつけたいところです。

※見返してみるとmixinを説明するのに使ったサンプルコードが雑すぎですね。もうちょっと実践的なものにすれば良かったかなと反省です