イメージ画像

こんにちは。アイレットデザイン事業部のマークアップ/フロントエンドエンジニアの工藤です。アイレットデザイン事業部ではINSIDE UI/UXと題して、所属デザイナーとエンジニアがデザイン・SEO・アクセシビリティ・UI/UXなどそれぞれスペシャリティのある領域に対する知見を幅広く発信しています。

今回は1. 良いCSSとは何かを振り返り、2. それを実現するための設計思想のひとつである BEM(Block Element Modifier) のメリットを改めて紹介するとともに、BEM導入にあたって必ずぶち当たる迷いがちな難所をパターン化して解決するためのヒントを書いていきます。後半が本題になりますので、前半は飛ばしてもらって大丈夫です。

そもそも良いCSSとは

さて、今日のweb開発においてはデザインカンプ通りにレイアウトできることができればOK…というところから一歩進んで、「良いCSSコード」を書く意識が大切になってきています。
なぜならwebサイトは多くの場合、作って終わりではなく数年運用するものです。この運用フェーズにおいて、新しいデザインを追加するなど二次開発が発生するかもしれません。良いCSSはそうした運用・保守のフェーズにも強いです。
そしてもう1つのポイントとして、フロントエンドのコーディングにおいてもGitやSlackなどのツールを利用し複数人で共同開発することが一般化しており、殊にコロナ禍に見舞われた2020年以降の日本においては一層リモートワーク/テレワークを導入の流れが加速していることがあります。そうした中においては、動けばいい/デザイン通りになっていればいい…ということを超えて、複数開発者の誰もが予測しやすい命名ルールがあり、共同開発を加速させる設計になっているものが良いCSSと言えます。以上の観点から優れたCSS設計を学び、チームの共通理解とする必要があります。

Phil Watsonの4原則

Googleのフロントエンド開発者であるPhil Walton氏は良いCSSとは以下の4つの特徴を備えているものであると提唱しています

  • Predictable(予測しやすい)
  • Maintainable(保守しやすい)
  • Reusable(再利用しやすい)
  • Scalable(拡張しやすい)

箇条書きだと掴みにくいですが、実際のコーディングを想像すれば理解しやすいかと思います。要は複数人でコードを書いたり、独立したUIパーツ/コンポーネントをスタイルガイド化したり、既存のコードをもとに新しいコードを書いたり、あるいは後任者に引き継ぎ容易なCSSコードが良いと言っていますね。これはCSSに限らずどの言語にも言えることです。

Nicolas GallagherのIdiomatic CSS

Twitterのフロントエンド開発者Nicolas Gallagher氏はIdiomatic CSSという一貫性のあるCSSを書くための原則を提唱しています。

“成功を収めるプロジェクトをうまく管理することの一つが自分でコードを書いて実現するということではない。もし多くの人があなたのコードを利用しているなら、仕様の中にはあなたの好みではなく、最大限に明確なコードを書くべきである。” – Idan Gazit

  • どんなに多くの人が貢献したとしても、どのコードも一人で書いたようにする。
  • 同意の上のスタイルを厳格に守る。
  • もし悩むようであれば常に現存する共通のパターンを利用する。

BEM

BEM

こうした開発現場の要請から生まれたCSS設計思想の一つがBEMです。
BEM(Block Element Modifier)は厳格で明瞭な命名規則を持ち、チーム開発や保守にも優れているCSS設計です。さらに、基本的にネストした親子関係のクラスを作らない特徴はCSSパフォーマンス向上に寄与します(ブラウザによるCSSのマッチングは右から左に行われるため、親子関係がない方が速くなる)。つまりPhil WatsonとNicolas Gallagher両氏の挙げる「良いCSS」の特徴をすべて備えていると言えます。

BEM自体は比較的枯れた技術で多くのドキュメントがありますので、このブログではBEMとは何か?については端折ります。

詳しくは以下のリンクをご覧ください

BEMで迷いがちな難所はパターン化して解決しよう

さて、ここからが本題です。クラス名がキモい以外は万能に見えるBEMですが、時として命名や設計に迷うことがあります。それをどのように解決すべきか考えていきます。

Blockの中の孫要素

例として1つのHTMLコードを示します。
カード型UIコンポーネントだと思ってください。

<div class="card">
  <div class="card__header">
    <h2 class="card__header__title">カードのタイトル</h2>
  </div>
  <div class="card__body">
    <img class="card__body__img" src="some-img.png" alt="画像" />
    <p class="card__body__text">テキストが入ります
      <a class="card__body__text__link" href="/somelink.html">リンクです</a>
    </p>
  </div>
</div>

.card__headerの中の.card__header__title、あるいは.card__bodyの中の.card__body__text.card__body__text__linkといったBlockの中の孫要素のクラス名がアンダースコア2つの連続で長大になっています。このままいけばネストが深くなるにしたがってさらに大変なことになりそうです。

これは以下のように解決できます。

<div class="card">
  <div class="card__header">
    <h2 class="card__title">カードのタイトル</h2>
  </div>
  <div class="card__body">
    <img class="card__img" src="some-img.png" alt="画像" />
    <p class="card__text">テキストが入ります
      <a class="card__link" href="/somelink.html">リンクです</a>
    </p>
  </div>
</div>

HTML上のネスト/親子関係は無視し、クラス名を.cardBlockの中のElementと考えるとスッキリします。あくまでBlock中心に考えるのがポイントです。

Sass側で&を使うか

さて、HTML側はこれでいいとして、Sass側の書き方は2通りが考えられます。

//Aパターン: 愚直に並べる

.card{
}
.card__header{
}
.card__body{
}
.card__title{
}
.card__title--primary{
}

と愚直に並べる形はKISSの原則に沿っており、後からクラス名で検索しやすいメリットがあります。

//Bパターン: &でネストする

.card {
  &__header {}
  &__body {}
  &__title {
    &--primary {}
  }
}

こちらはネスト記法でCSSが書けるSassの利点を生かすことができますね。
Sassの中ではクラスがネストしていますが、コンパイルされたCSSファイル上ではすべてのクラスが同じ単一の詳細度を保つことができます。

個人的にはこれは正解があるものではなくどちらの書き方でも良いと思います。冒頭で触れましたが、BEMはチーム開発が前提ですのでチームとして導入しやすい方を選ぶべきです。

BEM HelperなどVS Codeの拡張機能を使う前提ならネスト記法で統一した方が便利かもしれません。

ラッパークラスをどう書くか

BEMではラッパークラスをどうするかも迷うポイントです。
BEMの原則には反するが、.card__wrapperなどのクラス名を許容することも考えられます(下記Aパターン)。しかし、接頭辞l-を付けたレイアウト専用クラスを利用する方法(下記Bパターン)がベターでしょう。

<!-- Aパターン: BEMの原則に反するが、.card__wrapper クラスを許容する -->
<div class="card__wrapper">
  <div class="card">
    <div class="card__header">
      <h2 class="card__title">カードのタイトル</h2>
    </div>
    <div class="card__body">
      <img class="card__img" src="some-img.png" alt="画像" />
      <p class="card__text">テキストが入ります
        <a class="card__link" href="/somelink.html">リンクです</a>
      </p>
    </div>
  </div>
  <div class="card">
    <div class="card__header">
      <h2 class="card__title">カードのタイトル</h2>
    </div>
    <div class="card__body">
      <img class="card__img" src="some-img.png" alt="画像" />
      <p class="card__text">テキストが入ります
        <a class="card__link" href="/somelink.html">リンクです</a>
      </p>
    </div>
  </div>
</div>
<!-- Bパターン: 接頭詞 l- を付けたレイアウトクラスを利用 -->
<div class="l-card-wrapper">
  <div class="card">
    <div class="card__header">
      <h2 class="card__title">カードのタイトル</h2>
    </div>
    <div class="card__body">
      <img class="card__img" src="some-img.png" alt="画像" />
      <p class="card__text">テキストが入ります
        <a class="card__link" href="/somelink.html">リンクです</a>
      </p>
    </div>
  </div>
  <div class="card">
    <div class="card__header">
      <h2 class="card__title">カードのタイトル</h2>
    </div>
    <div class="card__body">
      <img class="card__img" src="some-img.png" alt="画像" />
      <p class="card__text">テキストが入ります
        <a class="card__link" href="/somelink.html">リンクです</a>
      </p>
    </div>
  </div>
</div>

これは名前空間の考え方を応用したもので、CSS界ではSMACCSが最初に取り入れた方法論です。
レイアウトクラスは通常、1. 並べ方をどうするか、2. マージンとパディングをどれだけ開けるか 3. 箱の大きさはどのくらいか 4. 背景色は何色か…といった限定された情報を持ち、主にコンポーネントの背景や箱として使われますから、FLOCSS的なファイル階層構造にした上でレイアウトクラスをCSSファイルの前半で読み込ませるようにしておけばカスケーディングな上書きによるレイアウト崩れも起きにくくなるでしょう。

レイアウトクラスの他にも、接頭辞によってCSSクラスの粒度管理をする方法は便利です。

  • l-:レイアウト用クラス
  • is-: 状態(state)を表すクラス
  • js-: JavaScriptに関するクラス
  • u-: ユーティリティクラス

個人的には上記4種類くらいで比較的ゆるい感じで使っています。c-でコンポーネントを表す記法もありますが、これは使っていません。コンポーネントを表すクラスはCSSの中でもっとも多く使われるため「接頭辞が何もなければコンポーネント」という理解で十分だと考えられるためです。

状態を表す方法

状態(state)を表す方法も2通り考えられるわけですが、これは先ほど紹介した名前空間を利用したis-activeの方が楽に管理できます。

<!--Aパターン modifierを使用->
<button type="button" class="button button--active">ここをクリック</button>
<!--Bパターン 名前空間を使用->
<button type="button" class="button is-active">ここをクリック</button>

クラスなしの要素へのスタイル指定を許容するか

たとえばフォントサイズや色が一意の平文に使う文字をクラスレスなp要素のみで使うことは許容できると考えられます。

<p>これは平文です。</p>
// foundation.scss などに指定
p {
  color: #333;
  font-size: 16px;
  line-height: 1.8;
}

逆に親子関係が発生する要素をクラスなしで使うのは避けるべきでしょう。

<div class="list">
  <ul>
    <li>
      <p>これはネストされたp要素です</p>
    </li>
  </ul>
</div>

上記のようなネストによってスタイルの変化が生じるのであれば、クラスをつけるべきです。

// これだとBEMの意味がない
.list {
  ul {
    li {
      p {
        color: #333;
        font-size: 18px;
                font-weight: bold
        line-height: 1.7;
      }
    }
  }
}

シングルクラスorマルチクラス

BEMはマルチクラスを許容しますので、マルチクラスで良いでしょう。言い換えるとスタイルの微差のためにすべてをシングルクラスにしようと頑張る必要はないところです。
ただ、長大なクラスの連続になる欠点はあります。

<button type="button" class="button button--border-black button--rounded button--size-full">
.button {
  position: relative;
  display: inline-block;
  width: 320px;
  height: 60px;
  padding: 0;
  cursor: pointer;
  background-color: #fff;
  border: 0;
  font-size: 14px;
  font-weight: bold;
  color: #333;
  text-decoration: none;
  &--border-black {
    border: 1px solid #333;
  }
  &--rounded {
    border-radius: 3px;
  }
  &--size-full {
    width: 100%;
  }
}

余談ですが下記のように.--からクラスを書くパターンでクラス名を短縮することも可能ですが、お勧めしません。この記法はIE11でエラーになるからです。日本は2022年現在もIE11のシェアがそれなりにあるため避けた方が無難です(2022年6月にサポート終了が発表されていますが)。

<!-- 残念ながらIEでエラー… -->
<button class="button --border-black --rounded --size-full" type="button">
</button>

Sassプレースホルダー%を使えばシングルクラスも可能に

modifierを使ったバリエーションがそんなにたくさんないケース(だいたい5以下)はシングルクラスで大丈夫です。
そういうケースではSassプレースホルダー%が便利です。
.buttonの共通部分を以下のようにSassプレースホルダーに記述し、@extendで引き継いで使用できます。
これならSassファイル内で同じ記述を繰り返すことがなく、DRY原則にも適います。

マルチクラス許容を原則としつつ、バリエーションが5以下ならシングルクラスでmodifierの変化による対応でOKの共通認識があれば問題ないかと思います。

//_button.scss
%button {
  position: relative;
  display: inline-block;
  width: 320px;
  height: 60px;
  padding: 0;
  font-weight: bold;
  font-size: 14px;
  color: #333;
  text-decoration: none;
  background-color: #fff;
  border: 0;
  cursor: pointer;
}

.button {
  @extend%button;
  &--primary {
    @extend%button;
    border: 1px solid #333;
  }
  &--secondary {
    @extend%button;
    border: 1px solid #aa00ee;
    border-radius: 3px;
  }
}

上記をコンパイルすると共通部分のみまとめられた以下のCSSが吐き出されます。

/*style.css*/
.button--secondary, .button--primary, .button {
position: relative;
display: inline-block;
width: 320px;
height: 60px;
padding: 0;
font-weight: bold;
font-size: 14px;
color: #333;
text-decoration: none;
background-color: #fff;
border: 0;
cursor: pointer;
}
.button--primary {
border: 1px solid #333;
}
.button--secondary {
border: 1px solid #aa00ee;
border-radius: 3px;
}

Sass側もCSS側もスッキリしてていい感じだと思いませんか?かつてはSassでこの記法が使えず、@at-rootを使った指定が必要でしたが、2022年現在ではアップデートにより必要なくなりました。もちろんDart-Sassでも大丈夫です。

参考
Sass (SCSS) BEMで書く時に @at-root は必要か?
まだ間に合う!node-sass(LibSass)から sass(Dart Sass)への移行

今回のINSIDE UI/UXはここまでです。予測性・保守性・再利用性・拡張性、さらにパフォーマンスに優れ、共同開発を加速させるCSS設計思想BEMを使って軽快なUI/UXを実現していきましょう!

P.S. アイレットではエンジニア、デザイナーを募集しています。詳しくは採用情報のページをご覧ください。