Hi. rigani_c です。

本記事では Rails における弊社 CSS チームの CSS 設計を、私の思想と併せて話します。
Sprockets を使うケースを想定しています。Sprockets は Rails 6 でもデフォルトのはず。。

また、これは未検証の設計です。
今後開発していくサービスにて導入予定の設計ですので、 大きな破綻がある可能性 があります。導入後に人柱報告しますね。

『壊れにくい CSS 設計』は全ての傷が致命傷となり得る

CSS を壊れないように運用していくのは極めて困難です。
CSS は雑に書いても目的のスタイルを作れちゃいますから、設計を理解しているメンバーがプロジェクトから離れると CSS の壊死は始まり、着実に広がっていきます。

???『なるほど完璧な設計っスねーっ 運用されないという点に目をつぶればよぉ~』

クラス名を繋ぎ合わせてプロジェクト全体に利用するような CSS 設計の場合、この壊死の治療は大変です。正常なスタイルを崩してしまわないか、広大な影響範囲を確認しなければなりません。どれだけ CSS が得意な人がプロジェクトに入ったところで、苦しみは同じです。
治療はどんどん先送りにされ、どろどろに腐りきったスタイルの出来上がりです。やったね。

私の CSS 設計思想

私が CSS 設計に必要だと感じていることは厳格なルールでも壊れにくさでもありません。
分かりやすいルール捨てやすさ です。

???『逆に考えるんだ 「すてちゃってもいいさ」と 考えるんだ』

デブリードマンを御存知でしょうか。
感染や壊死をした部位を除去する外科処置のことです。
Wikipedia には

感染、壊死組織は正常な肉芽組織の成長の妨げとなるため、デブリードマンは創傷外科治癒の原則である。

とあります。
これが CSS の運用においても必要であると考えています。アンパンマンの顔のようにリファクタリングできたら嬉しいですよね。

CSS でデブリードメントを可能にするのは何かしらのスコープです。確認可能な広さのスコープで、混ざり合わない境界が要るのです。

結局スコープかよ!って思いましたか?そうです。しかし、スコープは本当に重要なんです。あっ叩かないで...叩かないで...

Rails の SCSS ファイル生成

Rails では rails generate controller をすると、View ファイルと対応した SCSS ファイルが生成されます。
このまま SCSS ファイルに記述していくとスタイル規則はグローバルに放り出されますから、何かしらの設計を施します。BEM や OOCSS など。

私が初めに考えたのは、Controller + Action の組み合わせを利用したスコープでした。

<body data-controller="users" data-action="index">
  <!-- ... -->
</body>
[data-controller="users"][data-action="index"] {
  // ...
}

特別な役割であることを明確にするため、クラスでなく data 属性を利用しています。

悪くなさそうですが、名前空間や Layout への対応を考えるとルールが複雑化しそうです。

ファイルパススコープ

CSS チームやプロダクトメンバーとの議論の結果、生まれたのが ファイルパススコープ です。(私が勝手にそう呼んでるだけですが)

View ファイルと SCSS ファイルを対になるようにファイルパスを揃えて、スコープ用に data 属性を書くだけです。これなら運用も容易なはずです。

<%# app/views/users/show.html.erb %>
<div data-scope-path="users/show">
  <%# ... %>
</div>
// app/assets/stylesheets/scopes/users/show.scss
[data-scope-path="users/show"] {
  // ...
}

こうすることで、Vue の Scoped CSS のような追記しやすさ、捨てやすさを得ることが出来ます。
Rails だと View ファイルひとつでフロント 1 ページになってたりするので、「このページはこの CSS を変更するだけで OK 」って感じで気持ちよく CSS が書けます。

ただし、デブリードメントする場合に 1 ページ分の CSS を丸々書き直すことになるかもしれないので、シンプルなパワープレイができるようにチームの CSS 力を鍛えておきましょう 👺
闇の中で手探りに行うリファクタリングよりは遥かに健康的な仕事ができそうです。

スタイルの共通化は SCSS の @mixin$variable の機能を利用し、 mixins と variables ディレクトリ内に全て配置します。
ディレクトリ構造は次の項に任せます。

弊社で運用予定のルール

共通化の仕方やディレクトリ構成などをまとめておきます。命名は今後変わるかも。

ディレクトリ構成

管理画面とユーザ画面で配信する CSS を分ける場合

📂stylesheets
  📂admin_style
    📂mixins
      📄_button.scss
    📂scopes
      📂admin
        📂users
          📄edit.scss
          📄index.scss
          📄show.scss
    📂variables
      📄_color.scss
      📄_timing_function.scss
    📄application.css.scss
    📄reset.css
  📂media_style
    📁mixins
    📂scopes
      📂users
        📄edit.scss
        📄index.scss
        📄show.scss
    📁variables
    📄application.css.scss
    📄reset.css

配信する CSS をひとつに統合する場合

📂stylesheets
  📂mixins
    📄_button.scss
  📂scopes
    📂admin
      📂users
        📄edit.scss
        📄index.scss
        📄show.scss
    📂users
      📄edit.scss
      📄index.scss
      📄show.scss
  📂variables
    📄_color.scss
    📄_timing_function.scss
  📄application.css.scss
  📄reset.css

📁mixins
mixin を配置するディレクトリ

📁scopes
後述のファイルパススコープ対応のスタイルを配置するディレクトリ

📁variables
変数をまとめるディレクトリ

📄application.css.scss
順序を制御して読み込み、配信時に参照されるファイル

//= require 'reset'
//= require_self

@import 'variables/*';
@import 'mixins/*';
@import 'scopes/*';

📄reset.css
normalize.css や ress.css 等、リセット関連の名前に変更可
application.css.scss の最初の require に指定する

スコープ

  • View ファイルと SCSS ファイルを一対にする設計( mixin 等共通化は例外)
  • ファイル単位でスコープを固定する(ファイルパススコープ)
  • scopes ディレクトリに格納する

View → data-scope-path="views からの path"
SCSS → [data-scope-path="assets/stylesheets/**/scopes からの path"]

例: アクション

<%# app/views/users/show.html.erb %>
<div data-scope-path="users/show">
  <%# ... %>
</div>
// app/assets/stylesheets/scopes/users/show.scss
[data-scope-path="users/show"] {
  // ...
}

例: パーシャル

<%# app/views/users/hoge/_form.html.erb %>
<div data-scope-path="users/hoge/form">
  <%# ... %>
</div>
// app/assets/stylesheets/scopes/users/hoge/form.scss
[data-scope-path="users/hoge/form"] {
  // ...
}

例: レイアウト

<%# app/views/layouts/welcome/_header.html.erb %>
<div data-scope-path="layouts/welcome/header">
  <%# ... %>
</div>
// app/assets/stylesheets/scopes/layouts/welcome/header.scss
[data-scope-path="layouts/welcome/header"] {
  // ...
}

色変数(おまけ)

  • 全て HEX カラーを用いた変数にする
  • 色名だとわかる変数名にすること(例外は color サフィックスを付けること)
  • 透過色はアルファ値まで記述すること
  • キーワードに用意されている色名を変数名にする場合、値はキーワードと同値の HEX カラーを設定すること
// _color.scss
$white: #fff;
$black: #000;
$base-black: #333;
$wine-red: #7f1a1a;
$frosted-glass-color: rgba(#000, .92);
$sun-orange: #ff7500;
$vivid-red: #f32;

おわりの言葉

なんちゃって Scoped CSS したくない。Shadow DOM。。IE。。

今回の設計は @ra_gg さんの 『CSSをRailsとゆるふわにお付き合いさせる話』に似た思想です。CSSサイズの肥大化についても言及されているので、こちらもぜひ。

それでは。またね。