1. ホーム
  2. Sass

【Sass】&(アンパサンド)の使い方|入れ子で :hover や BEM のクラス名を書く方法

Share

Sass(SCSS)を使い始めて最初に便利さを実感するのが、セレクタの入れ子(ネスト)です。そして入れ子とセットで必ず出てくるのが &(アンパサンド)という記号です。&:hover のように書くのは見よう見まねでできても、「なぜ & が要るのか」「どういうときに省略できないのか」が曖昧なまま使っている方は多いのではないでしょうか。この記事では、入れ子がコンパイル後にどう展開されるかを確認しながら、& の役割と BEM のクラス名を組み立てる書き方、そして入れ子を深くしすぎないための考え方までを解説します。

入れ子はコンパイルでどう展開されるか

まず & のない、素直な入れ子から見ていきます。SCSS ではセレクタの中に別のセレクタを書けます。

style.scss
.card {
  padding: 16px;

  .title {
    font-size: 20px;
  }

  p {
    margin-top: 8px;
  }
}

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

style.css
.card {
  padding: 16px;
}
.card .title {
  font-size: 20px;
}
.card p {
  margin-top: 8px;
}

ポイントは、親のセレクタと子のセレクタが半角スペースで連結されることです。半角スペースは CSS では子孫セレクタ(子孫結合子)を意味しますから、「.card の中にある .title」という指定になります。入れ子はこの「スペースで繋ぐ」動きがすべての基本です。

ところが、実際に書きたいセレクタはスペースで繋ぐものばかりではありません。.card:hover や .card.is-active のように、スペースを入れずに同じ要素へ条件を足したい場面があります。ここで & の出番になります。

& は親セレクタに置き換わるプレースホルダー

& は、コンパイル時に親のセレクタそのものへ置き換わる記号です。.card の中で & と書けば、その位置に文字通り .card が入ります。そして & を使ったときはスペースが補われないので、&:hover と書けば .card:hover という、ひと続きのセレクタになります。

style.scss
.card {
  color: #333;

  &:hover {
    color: #0066cc;
  }

  &.is-active {
    border-color: #0066cc;
  }

  &::before {
    content: "";
  }
}
style.css
.card {
  color: #333;
}
.card:hover {
  color: #0066cc;
}
.card.is-active {
  border-color: #0066cc;
}
.card::before {
  content: "";
}

もし & を付けずに :hover { ... } とだけ書いてしまうと、通常の入れ子と同じ扱いになってスペースが挟まり、.card :hover(カードの中にある、ホバーされた子要素)というまったく別の意味になります。見た目の違いは半角スペース1つですが、効き方は大きく変わります。「スペースを入れたくないなら & が必要」と覚えておけば迷いません。

SCSS(.card の中での書き方)コンパイル後の CSS
.title { ... }.card .title
&:hover { ... }.card:hover
&.is-active { ... }.card.is-active
&::before { ... }.card::before
& > .title { ... }.card > .title
&__title { ... }.card__title

&__element と &–modifier で BEM のクラス名を組み立てる

& は疑似クラスやクラスを足すだけでなく、後ろに文字列をつなげて新しいクラス名を作ることもできます。これが BEM(ブロック__要素--修飾子 という命名規則)と相性が良く、多くのプロジェクトで使われている書き方です。

style.scss
.card {
  padding: 16px;
  border: 1px solid #ddd;

  // 要素(Element)
  &__title {
    font-size: 20px;
  }

  &__body {
    margin-top: 8px;
  }

  // 修飾子(Modifier)
  &--large {
    padding: 32px;
  }
}
style.css
.card {
  padding: 16px;
  border: 1px solid #ddd;
}
.card__title {
  font-size: 20px;
}
.card__body {
  margin-top: 8px;
}
.card--large {
  padding: 32px;
}

コンパイル後を見ると分かるとおり、.card__title は .card の子孫セレクタではなく、独立したひとつのクラスになっています。そのためセレクタが長くならず、詳細度も上がりません。ブロック名(.card)を変えたくなったときも、書き換えるのは先頭の1か所だけで済みます。

なお、& の後ろに文字列をつなげられるのは、親セレクタがクラス名や要素名のように「そのまま文字を足せる形」で終わっているときだけです。[type="text"] のような属性セレクタや :hover のような疑似クラスで終わっている親の中で &__title と書くと、Dart Sass はエラーを出します。

親セレクタの「前」に条件を付ける

& は行頭に置かなければならない決まりはありません。前に別のセレクタを書けば、「特定の状況下のときだけ」というスタイルを、対象のブロックの中にまとめて書けます。

style.scss
.card {
  background: #fff;

  // JavaScript が無効なときだけ
  .no-js & {
    display: none;
  }

  // ダークテーマのときだけ
  body.dark & {
    background: #222;
  }
}
style.css
.card {
  background: #fff;
}
.no-js .card {
  display: none;
}
body.dark .card {
  background: #222;
}

この書き方の利点は、.card に関する記述がファイルの中で1か所にまとまることです。ダークテーマ用のスタイルを別のファイルに切り出すと、片方だけ修正して食い違う、といった事故が起きやすくなります。

& を複数回使う

ひとつのセレクタの中に & を何度書いても構いません。よく使われるのが & + & で、「同じブロックが隣り合ったとき」を表せます。カードを縦に並べたときの間隔を、最初の1枚を除いて付けたい、といった指定に便利です。

style.scss
.card {
  padding: 16px;

  // 直前に .card があるとき(=2枚目以降)
  & + & {
    margin-top: 16px;
  }

  // .card の中の .card(入れ子になったとき)
  & & {
    border: none;
  }
}
style.css
.card {
  padding: 16px;
}
.card + .card {
  margin-top: 16px;
}
.card .card {
  border: none;
}

親セレクタが .card ひとつなら結果は単純ですが、.card, .panel のように複数のセレクタをまとめて書いている場合、& + & はすべての組み合わせ(.card + .card、.card + .panel、…)に展開されて数が増えます。カンマ区切りの親の中で & を何度も使うときは、出力される CSS を一度確認しておくと安心です。

入れ子は深くしすぎない

入れ子は HTML の構造をそのまま写せるので、つい何階層も掘りたくなります。しかし深い入れ子は、あとから困る CSS を生みます。目安としては、2〜3階層までに抑えるのが扱いやすい範囲です。

セレクタが長くなり、詳細度が上がる

.page .content .card .title span のようなセレクタが出力されると、それを上書きするにはさらに強いセレクタか !important が必要になります。詳細度の勝負が始まると、以後の修正がどんどん苦しくなります。また、そのスタイルが「その HTML 構造でしか効かない」状態になるため、同じカードを別の場所で使い回せません。

先に紹介した &__title による BEM 形式は、この点で優れています。何行入れ子にしても出力は単一クラスなので、詳細度は常に一定に保たれます。

&__title はクラス名で検索できなくなる

一方で、& による連結には実務上のはっきりしたデメリットがあります。ソースコードのどこにも .card__title という文字列が存在しないことです。HTML で class="card__title" を見つけて「このスタイルはどこで定義されているのか」と探しても、エディタの検索や grep では SCSS 側がヒットしません。__title だけで検索すれば見つかりますが、同名の要素が他のブロックにもあると候補が増えてしまいます。

これは書きやすさと追いやすさのトレードオフです。1つのブロックを1つのパーシャル(_card.scss)にまとめ、ファイル名からクラス名を推測できるようにしておくと、この弱点はかなり和らぎます。チームで作業するなら、&__ を使うか、クラス名を毎回フルで書くかを最初に決めておくと揉めません。

思った通りのセレクタにならないとき

スタイルが効かないときは、まずコンパイル後の CSS を開いて、実際に出力されたセレクタを目で確認するのが一番の近道です。そのうえで、よくある原因を挙げておきます。

& の付け忘れで余計なスペースが入っている

最も多いのがこれです。.is-active { ... } と書いたつもりが、出力は .card .is-active になっている、というパターンです。要素そのものに条件を足したいのか、子孫を指したいのかで & の要否が決まります。逆に & を付けすぎて .card.title(同じ要素が両方のクラスを持つ)になっているケースもあるので、出力側のスペースの有無を確認してください。

メディアクエリの中で使っている

@media はセレクタではないので、その中に書いた & はメディアクエリではなく、さらに外側のセレクタを参照します。下の例では &:hover が .card:hover になり、それが @media でくくられた形で出力されます。入れ子の途中に @media を挟んでも、セレクタの組み立て方は変わりません。

style.scss
.card {
  @media (min-width: 768px) {
    padding: 32px;

    &:hover {
      background: #f5f5f5;
    }
  }
}

ルートで & を書いている

どのセレクタの中にも入っていない状態(トップレベル)で & を書くと、参照すべき親がないためエラーになります。@mixin の中に & を書くことはできますが、その & が指すのは @include した場所の親セレクタです。ミックスインを定義した場所ではないので、トップレベルで @include すると同じエラーに行き当たります。

CSS ネイティブのネストとの違い

現在は CSS 自体にもネストの機能(CSS Nesting)があり、& も使えます。&:hover や & + & のような書き方は、Sass を通さずに CSS へ直接書けます。

ただし、両者の & は仕組みが違います。Sass の & はコンパイル時に親セレクタの文字列へ置き換わるのに対し、CSS の & は親セレクタと同じ要素にマッチするセレクタとして、そのまま実行時に評価されます。文字の置き換えではないため、CSS のネストでは &__title のように文字列をつなげて .card__title という新しいクラス名を作ることはできません。BEM のクラス名を組み立てる用途については、今のところ Sass ならではの機能だと考えておくとよいでしょう。

まとめ

入れ子はセレクタを半角スペースで連結する仕組みで、& はその入れ子の中で親セレクタに置き換わるプレースホルダーです。スペースを入れずに同じ要素へ条件を足したいとき、つまり &:hover や &.is-active のようなセレクタを書きたいときに & が必要になります。後ろに文字列をつなげれば &__title や &--large で BEM のクラス名を組み立てられ、出力は単一クラスなので詳細度も上がりません。前に置けば .no-js & のような条件付きスタイルをブロックの中にまとめられます。便利な一方で、入れ子を深くするとセレクタが長く・強くなり、&__ による連結はクラス名での検索が効かなくなります。2〜3階層までを目安にし、迷ったらコンパイル後の CSS を確認する習慣をつけておけば、入れ子で困ることはほとんどなくなるはずです。

参考ページ