POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSXFacebook
Victor Ayomipo

本記事は、原著者の許諾のもとに翻訳・掲載しております。

クイックサマリー:この記事の目的は、CSSカスケードレイヤーを既存のレガシーなコードベースに統合するプロセスを、ありのままに全てお伝えすることです。具体的には、何も壊さないように既存のCSSをリファクタリングしてカスケードレイヤーを使えるようにする方法について解説します。


Stephenie Eckles氏の記事「Getting Started With CSS Cascade Layers」を読めば、いつでも素晴らしい概要を学べます。しかし、この記事では、カスケードレイヤーを実際のコードに統合する体験、その長所、短所、そしてスパゲッティコードとの格闘まで、全てお話ししたいと思います。

よくある解説記事のようにサンプルプロジェクトを用意することもできます。しかし現実の世界はそんな風にはいきません。なぜか動いているけれど、誰もその理由を知らないようなスタイルが書かれたコードを引き継ぐ、といった感じで、実際に手を汚してみたいのです。

カスケードレイヤーを使っていないプロジェクトを見つけるのは簡単でした。難しいのは、詳細度や構成に問題を抱えるほど散らかっていて、なおかつカスケードレイヤー統合のさまざまな側面を説明できるくらい幅広いプロジェクトを見つけることでした。

皆さま、Drishtant Ghosh氏が作成した、こちらのDiscordボットのWebサイトをご紹介します。Drishtant氏がご自身の作品を事例として使うことを許可してくださったことに、深く感謝します。このプロジェクトは、ナビゲーションバー、ヒーローセクション、いくつかのボタン、モバイルメニューを備えた、典型的なランディングページです。

拡大プレビュー

外見は完璧に見えるのがお分かりでしょう。しかし、その裏側にあるCSSスタイルを見てみると、事態は面白くなってきます。

プロジェクトを理解する

あちこちで@layerを使い始める前に、まずは私たちが扱う対象をしっかりと理解しましょう。GitHubリポジトリをクローンし、今回はCSSカスケードレイヤーを扱うことに主眼を置いているため、index.htmlindex.cssindex.jsの3つのファイルで構成されるメインページにのみ焦点を当てます。

: このチュートリアルが冗長になりすぎるのを避けるため、プロジェクトの他のページは含めていません。しかし、実験として他のページをリファクタリングしてみるのも良いでしょう。

index.cssファイルは450行以上のコードがあり、ざっと目を通しただけでも、いくつかの懸念点が見て取れます。

  • 同じHTML要素を指す同じセレクタで、多くのコードが重複している。
  • #idセレクタがかなり多い。これについてはCSSで使うべきではないと主張する人もいるでしょう(私もその一人です)。
  • #botLogoが2回定義されており、その間は70行以上も離れている。
  • !importantキーワードがコード全体で安易に使われている。

それでも、サイトは機能しています。ここには「技術的に」間違っていることは何もありません。これこそが、CSSが巨大で美しいモンスターであるもう一つの理由です。エラーが表に出てこないのです!

レイヤー構造を計画する

さて、「全てのスタイルを@layer legacyのような単一のレイヤーにまとめてしまえば、それで終わりじゃないか?」と考える人もいるかもしれません。

それでもいいですが…私はそうすべきではないと思います。

考えてみてください。もしlegacyレイヤーの後にさらにレイヤーが追加された場合、それらの新しいレイヤーはlegacyレイヤーに含まれるスタイルを上書きするはずです。なぜなら、レイヤーの詳細度は優先順位によって整理されており、後から宣言されたレイヤーほど高い優先順位を持つからです。

/* newの方が詳細度が高い */
@layer legacy, new;

/* legacyの方が詳細度が高い */
@layer new, legacy;

とはいえ、このサイトの既存スタイルでは!importantキーワードが多用されていることを忘れてはなりません。そうなると、カスケードレイヤーの順序は逆転します。ですから、レイヤーが次のように定義されていても、

@layer legacy, new;

!importantが宣言されたスタイルがあると、状況は一変します。この場合、優先順位は次のようになります。

  1. legacyレイヤー内の!importantスタイル(最も強力)
  2. newレイヤー内の!importantスタイル
  3. newレイヤー内の通常スタイル
  4. legacyレイヤー内の通常スタイル(最も弱い)

この点だけは、はっきりさせておきたかったのです。では、続けましょう。

カスケードレイヤーは、各レイヤーが明確な責務を持ち、後のレイヤーが常に勝つという明確な順序を作ることで詳細度を管理します。

そこで私は、5つの異なるレイヤーに分割することにしました。

  • reset: box-sizingやmargin、paddingといったブラウザのデフォルトスタイルのリセット。
  • base: bodyh1paなどのHTML要素のデフォルトスタイル。デフォルトのタイポグラフィや色も含む。
  • layout: 要素の配置を制御するための、主要なページ構造に関するもの。
  • components: ボタン、カード、メニューなど、再利用可能なUIセグメント。
  • utilities: 一つのことだけをうまくこなす、単一目的のヘルパー修飾子。

これはあくまで私がスタイルを分割し、整理するのが好きな方法です。例えば、Zell Liew氏は、レイヤーとして定義できる4つの異なる分類を持っています

さらに、物事をサブレイヤーに分割するという考え方もあります。

@layer components {
  /* sub-layers */
  @layer buttons, cards, menus;
}

/* or this: */
@layer components.buttons, components.cards, components.menus;

これも便利かもしれませんが、物事を過度に抽象化したくはありません。この戦略は、明確に定義されたデザインシステムを対象とするプロジェクトには、より適しているかもしれません。

私たちが活用できるもう一つのことは、レイヤー化されていないスタイルと、カスケードレイヤーに含まれないスタイルが最も高い優先順位を持つという事実です。

@layer legacy { a { color: red !important; } }
@layer reset { a { color: orange !important; } }
@layer base { a { color: yellow !important; } }

/* unlayered */
a { color: green !important; } /* highest priority */

しかし、私は全てのスタイルを明確なレイヤーで整理しておくという考え方が好きです。少なくともこの文脈においては、物事をモジュール化し、保守しやすく保つことができます。

それでは、このプロジェクトにカスケードレイヤーを追加していきましょう。

カスケードレイヤーを統合する

まず、ファイルの先頭でレイヤーの順序を定義する必要があります。

@layer reset, base, layout, components, utilities;

これにより、どのレイヤーがどのレイヤーよりも優先されるかが簡単に分かります(左から右に行くにつれて優先度が高くなります)。これで、セレクタの詳細度ではなく、レイヤーの責務という観点で考えられるようになります。ここからは、スタイルシートを上から下へと進めていきます。

最初に気づいたのは、PoppinsフォントがHTMLとCSSの両方のファイルでインポートされていたことです。フォントを素早く読み込むためには、一般的にHTMLでのインポートが推奨されるため、CSSのインポートを削除し、index.htmlの方を残しました。

次はユニバーサルセレクタ(*)のスタイルです。これには@layer resetに最適な、古典的なリセットスタイルが含まれています。

@layer reset {
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
}

それが片付いたら、次はbodyセレクタです。これには背景やフォントといったプロジェクトの核となるスタイルが含まれているので、@layer baseに入れます。

@layer base {
  body {
    background-image: url("bg.svg"); /* 分かりやすいようにbg.svgにリネーム */
    font-family: "Poppins", sans-serif;
    /* ... other styles */
  }
}

ここでの私の方針は、「baseレイヤーのスタイルは基本的にドキュメント全体に影響を与えるもの」とすることです。今のところ、ページの表示崩れなどは起きていません。

IDをクラスに置き換える

body要素セレクタの次にあるのは、IDセレクタ#loaderとして定義されているページローダーです。

none

私は、可能な限りIDセレクタよりもクラスセレクタを使うべきだと考えています。デフォルトで詳細度を低く保つことができ、詳細度の競合を防ぎ、コードをはるかに保守しやすくするからです。

そこで、index.htmlファイルを開き、id="loader"を持つ要素をclass="loader"にリファクタリングしました。その過程で、id="page"を持つ別の要素を見つけたので、それも同時に変更しました。

index.htmlファイルにいる間に、いくつかのdiv要素で閉じタグが欠けていることに気づきました。ブラウザがそれに対して寛容なのは驚きです。ともかく、それらをクリーンアップし、<script>タグを.heading要素の外に移動させて、bodyの直接の子要素にしました。スクリプトの読み込みをこれ以上困難にするのはやめましょう。

IDをクラスに移行して詳細度の条件をそろえたので、これらをcomponentsレイヤーに入れることができます。ローダーはまさに再利用可能なコンポーネントですからね。

@layer components {
  .loader {
    width: 100%;
    height: 100vh;
    /* ... */
  }
  .loader .loading {
    /* ... */
  }
  .loader .loading span {
    /* ... */
  }
  .loader .loading span:before {
    /* ... */
  }
}

アニメーション

次はキーフレームです。これは少し厄介でしたが、最終的にアニメーションを独自の新しい5番目のレイヤーに分離し、それを含むようにレイヤーの順序を更新することにしました。

@layer reset, base, layout, components, utilities, animations;

しかし、なぜanimationsを最後のレイヤーに置くのでしょうか?それは、アニメーションは一般的に最後に実行されるものであり、スタイルの競合の影響を受けるべきではないからです。

プロジェクトのスタイルから@keyframesを検索し、それらを新しいレイヤーに放り込みました。

@layer animations {
  @keyframes loading {
    /* ... */
  }
  @keyframes loading2 {
    /* ... */
  }
  @keyframes pageShow {
    /* ... */
  }
}

これにより、静的なスタイルと動的なスタイルが明確に区別され、再利用性が確保できます。

レイアウト

#pageセレクタも#idと同じ問題を抱えていましたが、先ほどHTMLで修正したので、これを.pageに変更し、layoutレイヤーに入れることができます。その主な目的はコンテンツの初期の表示状態を制御することだからです。

@layer layout {
  .page {
    display: none;
  }
}

カスタムスクロールバー

これらはどこに置くべきでしょうか?スクロールバーはサイト全体で持続するグローバルな要素です。これはグレーゾーンかもしれませんが、グローバルでデフォルトの機能であるため、@layer baseに完璧に収まると言えるでしょう。

@layer base {
  /* ... */
  ::-webkit-scrollbar {
    width: 8px;
  }
  ::-webkit-scrollbar-track {
    background: #0e0e0f;
  }
  ::-webkit-scrollbar-thumb {
    background: #5865f2;
    border-radius: 100px;
  }
  ::-webkit-scrollbar-thumb:hover {
    background: #202225;
  }
}

また、見つけ次第!importantキーワードも削除していきました。

ナビゲーション

nav要素は非常に分かりやすいです。ナビゲーションバーの位置と寸法を定義する主要な構造コンテナだからです。これは間違いなくlayoutレイヤーに入れるべきです。

@layer layout {
  /* ... */
  nav {
    display: flex;
    height: 55px;
    width: 100%;
    padding: 0 50px; /* 一貫した左右のpadding */
    /* ... */
  }
}

ロゴ

ロゴに関連するスタイルブロックが3つあります。nav .logo.logo img#botLogoです。これらの名前は冗長であり、継承によるコンポーネントの再利用性の恩恵を受けることができます。

私は次のようにアプローチしています。

  1. nav .logoは、ロゴが他の場所でも再利用できることを考えると、詳細度が高すぎます。navを削除して、セレクタを単に.logoにしました。そこには!importantキーワードもあったので、削除しました。
  2. 以前は柔軟性の低い絶対配置で設定されていた.logo imgを配置しやすくするため、.logoをFlexboxコンテナに更新しました。
  3. #botLogo IDは2回宣言されていたので、2つのルールセットを1つに統合し、.botLogoクラスにすることで詳細度を下げました。そしてもちろん、HTMLを更新してIDをクラスに置き換えました。
  4. .logo imgセレクタは.botLogoとなり、ロゴの全ての要素をスタイリングするためのベースクラスになります。

そして、残ったのは次のコードです。

/* initially .logo img */
.botLogo {
  border-radius: 50%;
  height: 40px;
  border: 2px solid #5865f2;
}

/* initially #botLogo */
.botLogo {
  border-radius: 50%;
  width: 180px;
  /* ... */
}

違いは、一方がナビゲーションで、もう一方がヒーローセクションの見出しで使われている点です。2つ目の.botLogoは、.heading .botLogoセレクタで少し詳細度を上げることで変換できます。ついでに、重複しているスタイルも整理しておきましょう。

ロゴを再利用可能なコンポーネントにうまく変えることができたので、コード全体をcomponentsレイヤーに配置しましょう。

@layer components {
  /* ... */
  .logo {
    font-size: 30px;
    font-weight: bold;
    color: #fff;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .botLogo {
    aspect-ratio: 1; /* widthに合わせて正方形の寸法を維持 */
    border-radius: 50%;
    width: 40px;
    border: 2px solid #5865f2;
  }
  .heading .botLogo {
    width: 180px;
    height: 180px;
    background-color: #5865f2;
    box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5);
    /* ... */
  }
}

これは少し手間がかかりました!しかしこれで、ロゴは新しいレイヤーアーキテクチャに完璧にフィットするコンポーネントとして、適切に設定されました。

ナビゲーションリスト

これは典型的なナビゲーションのパターンです。非順序リスト(<ul>)を、全てのリスト項目を同じ行に水平に(折り返しを許可して)表示する柔軟なコンテナに変えます。これは再利用可能なナビゲーションの一種であり、componentsレイヤーに属します。しかし、追加する前に少しリファクタリングが必要です。

既に.mainMenuクラスがあるので、これを活用しましょう。nav ulセレクタを全てそのクラスに置き換えます。繰り返しますが、これにより詳細度を低く保ちつつ、その要素が何をするのかがより明確になります。

@layer components {
  /* ... */
  .mainMenu {
    display: flex;
    flex-wrap: wrap;
    list-style: none;
  }
  .mainMenu li {
    margin: 0 4px;
  }
  .mainMenu li a {
    color: #fff;
    text-decoration: none;
    font-size: 16px;
    /* ... */
  }
  .mainMenu li a:where(.active, .hover) {
    color: #fff;
    background: #1d1e21;
  }
  .mainMenu li a.active:hover {
    background-color: #5865f2;
  }
}

コードには、小さい画面でナビゲーションが折りたたまれたときに、「開く」状態と「閉じる」状態を切り替えるために使われるボタンも2つあります。これは.mainMenuコンポーネントに特有のものなので、全てをcomponentsレイヤーにまとめておきます。その過程で、セレクタを結合・簡略化して、よりクリーンで読みやすいスタイルにすることができます。

@layer components {
  /* ... */
  nav:is(.openMenu, .closeMenu) {
    font-size: 25px;
    display: none;
    cursor: pointer;
    color: #fff;
  }
}

また、CSS内の他のいくつかのセレクタがHTMLのどこにも使われていないことに気づきました。そこで、コードをきれいな状態に保つために、それらのスタイルを削除しました。これを行うための自動化された方法もあります。

メディアクエリ

メディアクエリは専用のレイヤー(@layer responsive)を持つべきでしょうか、それとも対象要素と同じレイヤーにあるべきでしょうか?このプロジェクトのスタイルをリファクタリングしている間、私はこの問題に本当に苦労しました。いくつか調査とテストを行った結果、私の判断は後者、つまりメディアクエリは影響を与える要素と同じレイヤーにあるべきだ、というものです。

私の理由は、それらを一緒に保つことで、

  • レスポンシブスタイルと、そのベースとなる要素のスタイルを一緒に維持できる。
  • 上書きが予測可能になる。
  • 現代のWeb開発で一般的なコンポーネントベースのアーキテクチャとうまく調和する。

しかし、これはレスポンシブロジックがレイヤー間に散らばることも意味します。ですが、要素がスタイリングされるレイヤーと、そのレスポンシブな振る舞いが管理されるレイヤーとの間にギャップがあるよりはマシです。私にとって、それは避けたいアプローチです。なぜなら、あるレイヤーでスタイルを更新し、それに対応するレスポンシブスタイルをresponsiveレイヤーで更新し忘れがちだからです。

もう一つの大きなポイントは、同じレイヤー内のメディアクエリは、その要素と同じ優先順位を持つということです。これは、CSSカスケードをシンプルで予測可能に保ち、スタイルの競合をなくすという私の全体的な目標と一致しています。

さらに、CSSネスティング構文は、メディアクエリと要素の関係を非常に明確にします。componentsレイヤーにメディアクエリをネストした場合の見た目の省略例を次に示します。

@layer components {
  .mainMenu {
    display: flex;
    flex-wrap: wrap;
    list-style: none;
  }
  @media (max-width: 900px) {
    .mainMenu {
      width: 100%;
      text-align: center;
      height: 100vh;
      display: none;
    }
  }
}

これにより、コンポーネントの子要素のスタイル(例:nav .openMenunav .closeMenu)もネストできます。

@layer components {
  nav {
    &.openMenu {
      display: none;
      
      @media (max-width: 900px) {
        &.openMenu {
          display: block;
        }
      }
    }
  }
}

タイポグラフィとボタン

.title.subtitleはタイポグラフィのコンポーネントと見なせるので、それらと関連するレスポンシブスタイルは、ご想像の通り、componentsレイヤーに入ります。

@layer components {
  .title {
    font-size: 40px;
    font-weight: 700;
    /* etc. */
  }
  .subtitle {
    color: rgba(255, 255, 255, 0.75);
    font-size: 15px;
    /* etc.. */
  }
  @media (max-width: 420px) {
    .title {
      font-size: 30px;
    }
    .subtitle {
      font-size: 12px;
    }
  }
}

ボタンについてはどうでしょう?多くのWebサイトと同様に、このサイトにもそのコンポーネントのための.btnクラスがあるので、それらもそこに入れてしまいましょう。

@layer components {
  .btn {
    color: #fff;
    background-color: #1d1e21;
    font-size: 18px;
    /* etc. */
  }
  .btn-primary {
    background-color: #5865f2;
  }
  .btn-secondary {
    transition: all 0.3s ease-in-out;
  }
  .btn-primary:hover {
    background-color: #5865f2;
    box-shadow: 0px 0px 8px 2px rgba(88, 101, 242, 0.5);
    /* etc. */
  }
  .btn-secondary:hover {
    background-color: #1d1e21;
    background-color: rgba(88, 101, 242, 0.7);
  }
  @media (max-width: 420px) {
    .btn {
      font-size: 14px;
      margin: 2px;
      padding: 8px 13px;
    }
  }
  @media (max-width: 335px) {
    .btn {
      display: flex;
      flex-direction: column;
    }
  }
}

最後のレイヤー

まだutilitiesレイヤーには触れていませんでしたね!私はこのレイヤーを、特定の目的のために設計されたヘルパークラス用に確保しています。例えばコンテンツを非表示にしたり、このケースでは、ぴったりな.noselectクラスがあります。これには、要素の選択を無効にするという単一の再利用可能な目的があります。

というわけで、これが私たちのutilitiesレイヤーにおける唯一のスタイルルールになります。

@layer utilities {
  .noselect {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -webkit-user-drag: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
}

以上です!私たちは実在するプロジェクトのCSSを、CSSカスケードレイヤーを使うように完全にリファクタリングしました。作業開始時点のコードと最終的なコードを比較することができます。

全てが簡単だったわけではない

カスケードレイヤーでの作業が困難だったというわけではありませんが、その過程でいくつかの厄介な点があり、一度立ち止まって自分が何をしているのかを慎重に考えさせられました。

作業中にいくつかのメモを取りました。

  • 既存のプロジェクトでどこから手をつけるべきかを判断するのは難しい。 しかし、最初にレイヤーを定義し、その優先順位レベルを設定することで、既存のCSSに完全には慣れていなくても、特定のスタイルをどのように、どこに移動させるかを決定するためのフレームワークができました。これにより、自分を疑ったり、余分で不要なレイヤーを定義したりする状況を避けることができました。

  • ブラウザサポートは依然として問題! 私がこれを書いている時点で、カスケードレイヤーは94%のサポート率を誇りますが、あなたのサイトがレイヤー化されたスタイルをサポートできないレガシーブラウザに対応する必要があるかもしれません。

  • メディアクエリがプロセスの中でどこに当てはまるのかが明確ではなかった。 メディアクエリは、それらが最も効果的に機能する場所を見つけるという難題を私に突きつけました。セレクタと同じレイヤーにネストするのか、それとも完全に別のレイヤーにするのか?ご存じの通り、私は前者を選びました。

  • !importantキーワードの扱いは、まるで綱渡りのようです。これはレイヤーの優先順位システム全体を反転させてしまうのですが、このプロジェクトでは至る所で使われていました。これらを一つずつ取り除いていくと、既存のCSSアーキテクチャが崩れていきます。そのため、コードをリファクタリングしつつ既存の挙動を壊さないように修正し、スタイルが最終的にどう適用される(カスケードする)のかを正確に把握する、というバランス感覚が求められます。

全体として、CSSカスケードレイヤーのためにコードベースをリファクタリングすることは、一見すると少し気が遠くなる作業です。しかし重要なのは、物事を複雑にしているのはレイヤーそのものではなく、既存のコードベースであると認識することです

新しいアプローチがエレガントであっても、誰かの既存のアプローチを新しいものに完全に刷新するのは難しいものです。

カスケードレイヤーが役立った点(と、そうでもなかった点)

レイヤーを確立したことで、コードは間違いなく改善されました。未使用のスタイルや競合するスタイルを削除できたので、パフォーマンスベンチマークにもいくつか改善が見られるはずですが、真の価値は、より保守しやすくなったスタイル一式にあります。必要なものを見つけ、特定のスタイルルールが何をしているのかを把握し、今後新しいスタイルをどこに挿入すればよいかが、より簡単になりました。

同時に、カスケードレイヤーが銀の弾丸のような解決策だとは言いません。忘れてはならないのは、CSSはそれがクエリするHTML構造と本質的に結びついているということです。もし扱っているHTMLが構造化されておらず、divの乱用に苦しんでいるのであれば、その混乱を解きほぐす努力はより大きくなり、同時にマークアップの書き換えも伴うと見て間違いないでしょう。

カスケードレイヤーのためのCSSリファクタリングは、メンテナンス性の向上だけでも、間違いなくその価値があります

ゼロから始めて、作業を進めながらレイヤーを定義していく方が「簡単」かもしれません。なぜなら、整理すべき継承されたオーバーヘッドや技術的負債が少ないからです。しかし、既存のコードベースから始めなければならない場合は、まずスタイルの複雑さを解きほぐし、どの程度のリファクタリングが必要かを正確に判断する必要があるかもしれません。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。