CSSコーディング規約を導入して1年半運用した話

f:id:vasilyjp:20180927090637j:plain

フロントエンジニアの茨木です。

皆様はCSSを書く際にコーディング規約を意識しているでしょうか。かつて、弊社にはCSSのコーディング規約が存在せず、CSSファイルの肥大化・クラス命名規則の不統一が発生していました。メンテナンスが難しくなってきた為、1年半ほど前にCSSコーディング規約を設けました。若干のルール追加を伴いながら、現在まで問題なく運用できています。本記事ではフロントエンドで運用しているCSSのコーディング規約に関して紹介します。

導入環境

本記事では以下の環境を前提にしています。

  • Ruby on Rails 5.0
  • Sass 3.4
  • Slim 3.0

CSSコーディング規約のコンセプト

初めにチームでヒアリングを行い、以下のようなコンセプトを決定しました。

  • 読みやすい、書きやすいクラス名やタグ構造にする
  • スタイルを再利用できる
  • セレクタによるスタイルの競合を少なくする

既存の設計手法であるBEM*1やSMACSS*2などの検討を初めに行いました。検討の結果、最もコンセプトに近かったSMACSSを基に、チームの実情に即したルールを独自で設けることにしました。

CSSコーディング規約の概要

上記の3つのコンセプトに基づき、それぞれに対応する以下のルールを設けました。

  • ルール1: 読みやすく書きやすいクラス名やタグ構造にするためのルール
  • ルール2: スタイルを再利用するための構成要素のルール
  • ルール3: スタイル競合を少なくするためのルール

構成要素のルールはSMACSSのそれにかなり近いですが、他のルールは独自で定義したルールです。 これから、それぞれのルールについて紹介していきます。

ルール1: 読みやすく書きやすいクラス名やタグ構造にするためのルール

読みやすい、書きやすいクラス名やタグ構造にするためのルールです。タグ構造自体はHTMLの話ですが、CSSのコーディングに直接関わる部分なので規約に含めています。クラス名やタグ構造に関しては3つのルールを設けています。

ネスト構造をクラス名に含めない & クラス名はハイフン区切り

読みやすさ・書きやすさのためにネスト構造をクラス名に含めないことにしました。2単語以上のクラス名の場合は、CSSの言語仕様に則ってハイフン区切りで単語を区切ります。

class.html.slim

// 良い例
section.popular-users
  ul
    li.user
      span.name hogehoge
      span.age 25

// 悪い例
section.popular-users
  ul.users
    // 以下3行はネスト構造が含まれている
    li.users-user
      span.users-user-name hogehoge
      span.users-user-age 25

タグやクラス名をセマンティックに

タグやクラス名をセマンティックにするために、以下の3つのルールを設けています。

  • 文章構造に適したタグを可能な限り使う
    HTML5で定義されたheader、footer、navといったセマンティックなタグを積極的に使うようにしています。

  • クラス名は、必要最小限・セマンティックに命名する
    クラス名は、タグで表現しきれない意味を表すために必要最小限だけ振るようにしています。特にタグとクラス名の意味が重複しないように注意しています。

  • divやspanを使う場合は、必ずクラス名を振る
    divやspanはタグ自体が意味を持たないので、必ずクラス名を振って意味を与えるようにしています。デザイン上必要なdivには.wrapperなど目的が分かるクラス名を与えています。

correct_tag.html.slim

// 良い例
section.popular-users
  // デザイン目的のタグであることをクラス名で明示している
  .wrapper
    ul
      li.user
        span.name hogehoge
        span.age 25

// 悪い例
.popular-users
  // divには必ずクラスを振らなければならない
  div
    // タグとクラス名の意味が重複している
    ul.list
      li.user
        // spanには必ずクラスを振らなければならない
        span hogehoge
        // 文章構造が分からないクラス名
        span.small-number 25

文書構造上意味のないタグを可能な限り削減する

擬似要素などを積極的に使い、文書構造上意味のないタグを可能な限り削減しています。 HTML自体がかなり読みやすいものとなるのは勿論、タグが減少することによりスタイル競合のリスクが減少します。 擬似要素が役に立つ例として、以下の画像のような矢印付きリンクボタンを紹介します。

f:id:vasilyjp:20161209190841p:plain

矢印をimgタグで定義することは勿論可能です。しかし、矢印は文書として意味を持たないので、本来はHTML文書内に無い方がセマンティックです。このような場合に、タグ内にCSSで要素を追加できる擬似要素が有用です。以下にコードを示します。

link_with_arrow.html.slim

ul.links
  li
    a href='/page1' page1
  li
    a href='/page2' page2

link_with_arrow.sass

ul.links
  li
    list-style: none
    border-top: solid 1px #999
    width: 200px
    &:last-child
      border-bottom: solid 1px #999

  a
    display: block
    height: 50px
    line-height: 50px
    position:relative
    text-decoration: none
    // 擬似要素:afterにより右矢印のスタイルを定義
    &:after
      content: ''
      display: block
      position: absolute
      top: 0
      right: 10px
      width: 14px
      height: 50px
      background: url(//cdn.hoge.fuga/images/right_arrow.png) no-repeat center
      background-size: 6px 12px

ルール2: スタイルを再利用するための構成要素のルール

スタイルの再利用性を高めるために、ベース・モジュール・レイアウト・ユーティリティ・ステートという5つの構成要素を定義しています。ベース・モジュール・レイアウト・ステートに関しては、SMACSSと基本の考え方は同一です。ユーティリティはモジュールと類似していますが、スタンドアローンでない点がモジュールと異なります。ユーティリティは規約策定当初モジュールに含まれていました。運用していく中でモジュールの書き方が多様になってきたので、ユーティリティを別に定義しました。

ベース

各ページ共通のスタイル、各タグの基本となるスタイルを定義します。

_base.sass

header
  width: 100%

h1
  font-size: 30px

input 
  border: solid 1px #ccc

モジュール

画面上で再利用されるひとまとまりの要素に適用されるスタイルです。スタンドアローンで成り立ちます。 トップレベルに.m-モジュール名のクラスを必ず持つことを規定しています。mixinにする場合は、後述のユーティリティとの区別のため、m-から始まる名称にします。

_mixins.sass

@mixin m-items
  ul.m-items
    li.item
      img
        width: 100%
      .name
        font-size: 20px
  .
  .
  .

レイアウト

画面上の大枠のレイアウトを指定するための要素に適用されるスタイルです。l-で始まるクラス名で定義します。

_base.sass

.l-sub
  float: left
  width: 100px

.l-main
  margin-left: 120px

ユーティリティ

頻繁に使用されるスタイルの集合です。スタンドアローンでない点がモジュールと異なります。スタンドアローンでないことを明確にするために、必ず@mixinで定義すること、一切セレクタを持たないことを規定しています。この構成要素は当初の規約にはありませんでしたが、運用過程で暗黙に使われていました。次第にモジュールと混同されるようになってきたので、ルール化しました。

_mixins.sass

@mixin circle($diameter)
  width: $diameter
  height: $diameter
  border-radius: $diameter/2

util.sass

@import 'mixins'

.rank-badge
  @include circle(20px)
  background-color: #ccc

ステート

ステートは状態に応じてスタイルを上書きするためのクラスです。適用元のクラスを存置したままマルチクラスで定義します。ステートのクラス名は、適用元要素のクラス名を原則含みません。

state.sass

.m-like-button
  width: 100%
  // ステート
  &.active
    background-color: #ccc
    border: solid 1px #999

state.html.slim

ul.users
  li.user
    .m-like-button.active いいね済み
  li.user
    .m-like-button いいねする

ルール3: スタイル競合を少なくするためのルール

1ビュー1CSSのファイル構成

Railsではデフォルトで全CSSがapplication.cssにまとめられますが、スタイル競合のリスクが高まります。そのため、本規約ではビューごとにCSSの定義ファイルを別にしています。各ページ共通で用いるスタイルは別ファイルで定義し、@importで読み込むようにしています。

/app/assets/stylesheets/
 │
 ├ shared/
 │  │
 │  ├ _base.sass
 │  ├ _mixins.sass
 │  └ _reset.sass
 │
 ├ users/
 │  │
 │  ├ show.sass
 │  ├ create.sass
 .  .
 .  .
 .  .

show.sass

@import 'reset'
@import 'base'
@import 'mixins'

ルートでのクラス名/要素名単独のセレクタは禁止

ルートのセレクタはかなり影響範囲が大きいです。ルートでクラス名や要素名を単体で指定した場合には予期せぬ要素にスタイルを定義してしまう危険が高くなります。そのため、ルートでのクラス名/要素名単独のセレクタを禁止しています。このルールは、ベース・モジュール・レイアウト・ユーティリティには適用しません。このルールは当初暗黙のルールでしたが、今後メンバーが加わることを想定してルール化しました。

root_selector.sass

// 良い例
section.popular-articles
  h2
    font-size: 30px

// 悪い例1
.popular-articles
  h2
    font-size: 30px

// 悪い例2
section
  h2
    font-size: 30px

まとめ

規約により、読みやすさ・書きやすさ・再利用性が向上し、メンテナンスがとても楽になりました。BEMのような厳格な命名規則ではありませんが、セレクタやファイル構成の規約のおかげでスタイルの競合はほとんど発生しません。途中、若干のルール追加を伴いながら1年半運用できており、当初の目的は達成されたといえます。成功の要因としては、安全性と読みやすさ/書きやすさの双方を考慮した点、コードレビューを徹底している点が挙げられます。

読者の皆様も、CSSの実装中にスタイルの競合や再利用性の問題に遭遇することはよくあると思います。そのような場合、CSSの書き方だけでなくファイル構成やタグ構造も見直すことが解決につながるかもしれません。また、規約を継続運用するためには、安全性だけでなく読みやすい・書きやすいという点も重要かもしれません。

最後に

VASILYではWebの力で世の中を変えたいエンジニアを募集しています。 興味がある方は是非こちらからご応募ください。

*1:Methodology / BEM https://en.bem.info/methodology/

*2:Scalable and Modular Architecture for CSS https://smacss.com/

カテゴリー