HTML5Experts.jp

Sass 3.3で追加された「&」の新機能と@at-rootまとめ解説

10月12日にSass 3.3.0.rc.1が出ました。まだリリース候補ですが、どのような機能が追加されるのかはChangelogにあります。今回は「&」と@at-rootについて解説します。

HTML+CSSの命名規則にBEM方法論、もしくはHTML+CSS向けに派生したMindBEMdingを取り入れる方が増えてきているようです(筆者は使っていませんが…)。「&」の新機能と@at-rootは、このBEMのためといっても過言ではありません。

Sass 3.2の「&」

「&」は親セレクタを参照する特別なキーワードとして、Sass 3.3よりも前からありましたが、擬似クラスや擬似要素、セレクタの連結など、用途が限られていました。

// Sass 3.3よりも前の「&」の用途の例
.foo {
    &:hover  { ... } //-> .foo:hover
    &:after  { ... } //-> .foo:after
    &.bar    { ... } //-> .foo.bar
    & + .bar { ... } //-> .foo + .bar
    & > .bar { ... } //-> .foo > .bar
    .bar & { ... }   //-> .bar .foo
}

「&」を使って、親のクラス名を子のクラス名の一部にすることはできませんでした。

.foo {
    // .foo-barにしたいけど…
    &-bar { ... }  //-> ERROR
}

Sass 3.3の&

Sass 3.3からは「&」をより多くの場所で使うことができるようになりました。 前述の「親のクラス名を子のクラス名の一部にする」には次のようにします。

.foo {
    #{&}-bar { ... }  // #{}(インターポレーション)を使います
}

これをコンパイルすると次のようになります。

.foo .foo-bar { ... }

「&」はセレクタに使用するだけでなく、変数の値に指定することもできるようになりました。

.foo, .bar .baz {
    $selector: &;  // #{}は不要です
}

このようにした場合、$selectorには (“.foo”, (“.bar” “.baz”)) というようにリストとして格納されます(実際にtype-of()で調べると list が返ってきます)。括弧や引用符で囲まれていますが、実際にはそれらは外された状態で使用されます。

.foo, .bar .baz {
    $selector: &;
    content: $selector;
}

これをコンパイルすると次のようになります。

.foo, .bar .baz {
  content: .foo, .bar .baz;
}

「&」を使ってBEM

では、「&」を使ってBEMっぽく書いてみましょう。

.block {
    // .block__element
    #{&}__element {
        ...

    // .block__element--modifier
    #{&}--modifier { ... }
}

}

これをコンパイルすると次のようになります。

.block { ... }
.block .block__element { ... }
.block .block__element .block .block__element--modifier { ... }  // ?!

.block__elementの前の.blockはともかく、.block__element–modifierの一つ前にも.blockが出力されてしまっています…。 次のようにネストを減らして書くことで、これを回避することもできますが、、

.block {
    #{&}__element { ... }
    #{&}__element--modifier { ... }
}

この方法だと__elementを何度も書くことになってしまいます。.block__elementの前の.blockを出力しなくてもよいという運用ルールであれば、次に紹介する@at-rootで解決することができます。

@at-root

@at-rootの基本機能は、記述した場所より上のセレクタのネストを解除するというものです。
使い方は次のとおりです。

.foo {
    // 1つのルールセットのみに適用する
    @at-root .bar { ... }

// 複数のルールセットに適用する
@at-root {
    .baz { ... }
    .qux {
        ...

        .quux { ... }
    }
}

}

これをコンパイルすると次のようになります。

.foo { ... }
.bar { ... }
.baz { ... }
.qux { ... }
.qux .quux { ... }

.qux .quux { … }の.quxを取り除いて.quux { … }としたい場合は、.quuxの前にも@at-rootを記述します。

では、前節の問題を解決します。

.block {
    @at-root {
        // .block__element
        #{&}__element {
            ...

        @at-root {
            // .block__element--modifier
            #{&}--modifier { ... }
        }
    }
}

}

これをコンパイルすると次のようになります。

.block { ... }
.block__element { ... }
.block__element--modifier { ... }

ようやくすべてフラットにすることができました。

ただ、@at-rootの分だけネストが多くなったのが気になるかもしれません。
ネストを減らしたい方は@at-rootを隠蔽したミックスインを使うことを検討してみてください。

@at-rootの応用

@media内で@at-rootを使用した場合、そのルールセットは@media内に出力されます。

@media screen and (max-width:320px) {
    .foo {
        margin: 0;

    @at-root {
        .bar {
            padding: 0;
        }
    }
}

}

これをコンパイルすると次のようになります。

@media screen and (max-width: 320px) {
  .foo {
    margin: 0;
  }
  .bar {  /* @media内に出力される */
    padding: 0;
  }
}

@mediaの外に.barを出したい場合は、@at-root (without: …)を使います。

@at-root (without: … )

次のように@at-root (without: media)とすると@mediaの外に出すことができます。

@media screen and (max-width:320px) {
    .foo {
        margin: 0;

    @at-root (without: media) {
        .bar {
            padding: 0;
        }
    }
}

}

コンパイルすると次のようになります。

@media screen and (max-width: 320px) {
  .foo {
    margin: 0;
  }
}
.foo .bar {
  padding: 0;
}

@mediaの外には出されましたが.foo .bar { … }となってしまいました。.fooを取り除いて.bar { … }としたい場合は、withoutの値にruleを追加します。

@media screen and (max-width:320px) {
    .foo {
        margin: 0;

    @at-root (without: media rule) {
        .bar {
            padding: 0;
        }
    }
}

}

これをコンパイルすると次のようになります。

@media screen and (max-width: 320px) {
  .foo {
    margin: 0;
  }
}
.bar {
  padding: 0;
}

このようにwithoutと後述するwithには、スペース区切りで複数の値を指定することができます。

説明
mediaやsupportなど @ルールを除外する/しない
rule CSSルールセットを除外する/しない
all すべてを除外する/しない

前述の例では、@at-root(without: media rule)としましたが、必ずドキュメントルートに出力したい場合は(without: all)と記述した方が良いです。

@at-root(without: rule)は@at-rootのみ記述した場合と同じです。推測ですが、@at-rootには初期値として(without: rule)が設定されていると思われます。(without: media)とした場合は、初期値を上書きするので(without: rule media)のようには処理されなかったと考えられます。

また、withoutとwithは同時に指定することはできませんでした。(with: …)を指定した場合も初期値(without: rule)が上書きされてしまうようです。

@at-root(with: … )

では最後に(with: …)ですが、これに指定したもの以外が除外されます。以下にいくつかのサンプルを記載しておきますので、どのように出力されるか確認してみてください。

@media screen and (max-width:320px) {
    .foo {
        @supports ( display: flex ) {
            @at-root (with: rule) {
                .bar {
                    width: 0;
                }
            }
            @at-root (with: supports) {
                .baz {
                    height: 0;
                }
            }
            @at-root (with: media) {
                .qux {
                    margin: 0;
                }
            }
            @at-root (with: media rule) {
                .quux {
                    padding: 0;
                }
            }
        }
    }
}

コンパイルすると次のようになります。

@media screen and (max-width: 320px) {
  .qux {        /* (with: media) /
    margin: 0;
  }
  .foo .quux {  / (with: media rule) /
    padding: 0;
  }
}
.foo .bar {     / (with: rule) /
  width: 0;
}
@supports (display: flex) {
  .baz {        / (with: supports) */
    height: 0;
  }
}

おわりに

「&」の新機能と@at-rootを解説しましたが、いかがだったでしょうか。確かにメリットもありますが、「&」はクラス名の検索がしづらくなりますし、@at-rootは出力後のCSSがどうなるかが想像しにくいケースが出てくるなど、デメリットもあります。筆者としては、これらを利用する場合は用途を限定するなどのルールを設けたいと思いました。