JS における CSS を再考する

王林
王林オリジナル
2024-09-12 16:18:551222ブラウズ

0. はじめに

Web 開発の世界では、CSS はユーザー インターフェースを美しく機能的にするための重要な要素です。

しかし、Web アプリケーションの複雑さが増すにつれて、CSS 管理はますます困難なタスクになってきています。スタイルの競合、パフォーマンスの低下、メンテナンスの困難は、多くの開発者にとって懸念事項です。

これらの問題はプロジェクトの進行を妨げていませんか? (画像出典)

Rethinking CSS in JS

この記事では、これらの問題を解決するための新しいアプローチ、特に JS の CSSについて詳しく説明します。
CSS の歴史的背景から始まり、現代のスタイリング方法から将来のデザイン システムまで幅広いトピックをカバーしています。

記事の構成は次のとおりです:

  1. JS における CSS の定義と背景
    • 1. JS の CSS とは何ですか?
    • 2. JSのCSSの背景
  2. CSS とデザインの歴史的背景
    • 3. CSSの背景
    • 4.デザインの背景
    • 5.デザインシステムの背景
  3. スタイルマネジメント手法の分析と新たな提案
    • 6.スタイルはどのように管理されていましたか?
    • 7.スタイルはどのように管理すべきですか?
  4. JS での CSS の具体的な実装計画
    • 8. JS で CSS を使用する理由
    • 9.プロジェクト明朝体を紹介します
    • 10. JS の CSS フレンドリーな CSS
    • 11. JS のスケーラブルな CSS
  5. デザインシステムとの統合
    • 12.デザインシステム用の JS の CSS

特に、この記事では SCALE CSS 手法と StyleStack と呼ばれる新しい概念を紹介し、これらに基づいた明朝体プロジェクトを提案します。 CSS フレンドリーでスケーラブルな CSS を JS に実装することを目的としています。

この記事の最終的な目的は、より良いスタイル ソリューションの可能性を開発者、デザイナー、その他の Web プロジェクト関係者に提示することです。

さて、本文ではJSにおけるCSSの世界をさらに掘り下げていきましょう。長い道のりではありますが、新たなインスピレーションと挑戦の機会を提供できれば幸いです。

1. JSのCSSとは何ですか?

JS の CSS は、JavaScript (または TypeScript) コード内に CSS スタイルを直接記述できるようにする手法です。
個別の CSS ファイルを作成する代わりに、JavaScript ファイル内のコンポーネントと一緒にスタイルを定義できます。

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

const buttonStyles = (primary) => css({
  backgroundColor: primary ? "blue" : "white",
  color: primary ? "white" : "black",
  fontSize: "1em",
  padding: "0.25em 1em",
  border: "2px solid blue",
  borderRadius: "3px",
  cursor: "pointer",
});

function Button({ primary, children }) {
  return (
    <button css={buttonStyles(primary)}>
      {children}
    </button>
  );
}

function App() {
  return (
    <div>
      <Button>Normal Button</Button>
      <Button primary>Primary Button</Button>
    </div>
  );
}

JavaScript に統合できると確かに便利そうですね?

2. JSにおけるCSSの背景

JS の CSS は、Facebook 開発者である Vjeux による「React: CSS in JS – NationJS」というタイトルのプレゼンテーションで紹介されました。

CSS-in-JS が解決しようとした問題は次のとおりです:
Rethinking CSS in JS

具体的にはどのような問題がありますか?
そして、JS の CSS はそれをどのように解決するのでしょうか?

次の表に整理しました:

Problem Solution
Global namespace Need unique class names that are not duplicated as all styles are declared globally Use Local values as default
- Creating unique class names
- Dynamic styling
Implicit Dependencies The difficulty of managing dependencies between CSS and JS
- Side effect: CSS is applied globally, so it still works if another file is already using that CSS
- Difficulty in call automation: It's not easy to statically analyze and automate CSS file calls, so developers have to manage them directly
Using the module system of JS
Dead Code Elimination Difficulty in removing unnecessary CSS during the process of adding, changing, or deleting features Utilize the optimization features of the bundler
Minification Dependencies should be identified and reduced As dependencies are identified, it becomes easier
to reduce them.
Sharing Constants Unable to share JS code and state values Use JS values as they are, or utilize CSS Variables
Non-deterministic Resolution Style priority varies depending on the CSS loading order - Specificity is automatically calculated and applied
- Compose and use the final value
Breaking Isolation Difficulty in managing external modifications to CSS (encapsulation) - Encapsulation based on components
- Styling based on state
- Prevent styles that break encapsulation, such as .selector > *

But it's not a silverbullet, and it has its drawbacks.

  1. CSS-in-JS adds runtime overhead.
  2. CSS-in-JS increases your bundle size.
  3. CSS-in-JS clutters the React DevTools.
  4. Frequently inserting CSS rules forces the browser to do a lot of extra work.
  5. With CSS-in-JS, there's a lot more that can go wrong, especially when using SSR and/or component libraries.

Aside from the DevTools issue, it appears to be mostly a performance issue.
Of course, there are CSS in JS, which overcomes these issues by extracting the CSS and making it zero runtime, but there are some tradeoffs.
Here are two examples.

  1. Co‑location: To support co-location while removing as much runtime as possible, the module graph and AST should be analyzed and build times will increase. Alternatively, there is a method of abandoning co-location and isolating on a file-by-file basis, similar to Vanilla Extract.
  2. Dynamic styling restrictions: The combination of build issues and the use of CSS Variables forces us to support only some representations, like Styling based on props in Pigment CSS, or learn to do things differently, like Coming from Emotion or styled-components. Dynamicity is also one of the main metrics that can be used to distinguish between CSS in JS.

Therefore, pursuing zero(or near-zero) runtime in CSS-in-JS implementation methods creates a significant difference in terms of expressiveness and API.

3. The background of CSS

3.1 The Beginning of CSS

Where did CSS come from?
Early web pages were composed only of HTML, with very limited styling options.

<p><font color="red">This text is red.</font></p>
<p>This is <strong>emphasized</strong> text.</p>
<p>This is <em>italicized</em> text.</p>
<p>This is <u>underlined</u> text.</p>
<p>This is <strike>strikethrough</strike> text.</p>
<p>This is <big>big</big> text, and this is <small>small</small> text.</p>
<p>H<sub>2</sub>O is the chemical formula for water.</p>
<p>2<sup>3</sup> is 8.</p>

For example, the font tag could change color and size, but it couldn't adjust letter spacing, line height, margins, and so on.

You might think, "Why not just extend HTML tags?" However, it's difficult to create tags for all styling options, and when changing designs, you'd have to modify the HTML structure itself.
This deviates from HTML's original purpose as a document markup language and also means that it's hard to style dynamically.

If you want to change an underline to a strikethrough at runtime, you'd have to create a strike element, clone the inner elements, and then replace them.

const strikeElement = document.createElement("strike");
strikeElement.innerHTML = uElement.innerHTML;
uElement.parentNode.replaceChild(strikeElement, uElement);

When separated by style, you only need to change the attributes.

element.style.textDecoration = "line-through";

If you convert to inline style, it would be as follows:

<p style="color: red;">This text is red.</p>
<p>This is <span style="font-weight: bold;">bold</span> text.</p>
<p>This is <span style="font-style: italic;">italic</span> text.</p>
<p>This is <span style="text-decoration: underline;">underlined</span> text.</p>
<p>This is <span style="text-decoration: line-through;">strikethrough</span> text.</p>
<p>This is <span style="font-size: larger;">large</span> text, and this is <span style="font-size: smaller;">small</span> text.</p>
<p>H<span style="vertical-align: sub; font-size: smaller;">2</span>O is the chemical formula for water.</p>
<p>2<span style="vertical-align: super; font-size: smaller;">3</span> is 8.</p>

However, inline style must be written repeatedly every time.
That's why CSS, which styles using selectors and declarations, was introduced.

<p>This is the <strong>important part</strong> of this sentence.</p>
<p>Hello! I want to <strong>emphasize this in red</strong></p>
<p>In a new sentence, there is still an <strong>important part</strong>.</p>

<style>
strong { color: red; text-decoration: underline; }
</style>

Since CSS is a method that applies multiple styles collectively, rules are needed to determine which style should take precedence when the target and style of CSS Rulesets overlap.

CSS was created with a feature called Cascade to address this issue. Cascade is a method of layering styles, starting with the simple ones and moving on to the more specific ones later. The idea was that it would be good to create a system where basic styles are first applied to the whole, and then increasingly specific styles are applied, in order to reduce repetitive work.

Therefore, CSS was designed to apply priorities differently according to the inherent specificity of CSS Rules, rather than the order in which they were written.

/* The following four codes produce the same result even if their order is changed. */
#id { color: red; }
.class { color: green; }
h1 { color: blue; }
[href] { color: yellow; }

/* Even if the order is changed, the result is the same as the above code. */
h1 { color: blue; }
#id { color: red; }
[href] { color: yellow; }
.class { color: green; }

However, as CSS became more scalable, a problem arose..

3.2 Scalable CSS

Despite the advancements in CSS, issues related to scalability in CSS are still being discussed.
In addition to the issues raised by CSS in JS, several other obstacles exist in CSS.

  1. Code duplication: When writing media queries, pseudo-classes, and pseudo-elements, a lot of duplication occurs if logic is required.
  2. Specificity wars: As a workaround for name collisions and non-deterministic ordering, specificity keeps raising the specificity to override the style. You can have fun reading Specificity Battle!
  3. Lack of type-safety: CSS does not work type-safely with TypeScript or Flow.

These issues can be addressed as follows:

  1. コードの重複: CSS プリプロセッサなどでネストを使用します。
  2. 特異性戦争: アトミック CSS はプロパティごとに個別に定義されるため、読み込み順序と !重要を除いて同じ特異性を持ちます。
  3. タイプ セーフの欠如: タイプ セーフ サポートを備えた JS で CSS を使用するだけです。

レイアウトの表現は CSS のもう 1 つのハードルであり、さまざまなプロパティ間の相互作用によってさらに複雑になります。
Rethinking CSS in JS

CSS は一見シンプルに見えるかもしれませんが、習得するのは簡単ではありません。単純な中央揃えでも多くの人が苦労していることはよく知られています(1、2)。 CSS は一見シンプルに見えますが、その奥深さと微妙な違いにより、見た目よりも難しくなる場合があります。

たとえば、CSS での表示には、ブロック、インライン、テーブル、フレックス、グリッドなど、さまざまなレイアウト モデルがあります
次のプロパティを組み合わせて使用​​するときの複雑さを想像してみてください: ボックス モデル、レスポンシブ デザイン、フロート、位置決め、変換、書き込みモード、マスクなど。

プロジェクトの規模が大きくなるにつれて、DOM の位置決め、カスケード、特異性に関連する副作用により、作業はさらに困難になります。

レイアウトの問題は、適切に設計された CSS フレームワークを通じて対処する必要があり、前述したように、JS で CSS を使用してスタイルを分離すると、副作用を軽減できます。

ただし、このアプローチではすべての問題が完全に解決されるわけではありません。スタイルの分離は、各コンポーネントでのスタイル宣言の重複によるファイル サイズの増加や、アプリケーション全体での共通スタイルの一貫性維持の困難など、新たな副作用を引き起こす可能性があります。
これは、次に紹介する設計の組み合わせ爆発と一貫性の問題と直接衝突します。

今のところ、私たちはレイアウトに関する懸念を Bootstrap や Bulma などのフレームワークに委任し、管理面により重点を置くことができます

4. デザインの背景

CSS は本質的に、Web 開発でデザインを表現し実装するための強力なツールです。

UI/UX を作成する際には考慮すべき要素が多数あり、デザインで表現するには次の要素が重要です。
Rethinking CSS in JS

  1. ビジュアルデザイン
    • レイアウト: 画面の構造と要素の配置を決定します。要素間の間隔、配置、階層を考慮してください。
    • 色: ブランド アイデンティティとユーザー エクスペリエンスを考慮したカラー パレットを選択します。色彩理論の理解が必要です。
    • タイポグラフィ: 読みやすさとブランド イメージに合ったフォントとテキスト スタイルを選択します。
    • アイコンとグラフィック要素: 直感的で一貫性のあるアイコンとグラフィックをデザインします。
  2. インタラクションデザイン
    • ボタン、スライダー、スクロールバーなどの UI 要素の動作を設計します。
    • アニメーションとトランジション効果を通じて自然なユーザー エクスペリエンスを提供します。
    • さまざまな画面サイズに適応するレスポンシブデザインを検討してください。
  3. 情報アーキテクチャ
    • ユーザーが情報を簡単に見つけて理解できるように、コンテンツの構造と階層を設計します。
    • ユーザーが目的の場所に簡単に移動できるようにナビゲーション システムを設計します。
  4. アクセシビリティと使いやすさ
    • 多様なユーザーを考慮したインクルーシブデザインを追求します。 i18n も含めることができます。
    • 直感的で使いやすいインターフェイスを作成して、ユーザーの認知的負荷を軽減します。
  5. 一貫性とスタイルガイド
    • アプリケーション全体で一貫したデザイン言語を維持するためのスタイル ガイドを作成します。
    • 効率を高めるために再利用可能なコンポーネントとパターンを開発します。

さまざまな条件下でさまざまなデザイン要素を正確に表現することは、大きな課題となります。
デバイス (電話、タブレット、ラップトップ、モニター、テレビ)、入力デバイス (キーボード、マウス、タッチ、音声)、ランドスケープ/ポートレート モード、ダーク/ライト テーマ、ハイ コントラスト モード、国際化 (言語) を考慮する必要があることを考慮してください。 、LTR/RTL) など。
さらに、ユーザー設定に基づいて異なる UI を表示する必要がある場合があります。

したがって、組み合わせ爆発は避けられず、それらを 1 つずつ手動で実装することは不可能です。 (画像出典)

Rethinking CSS in JS

代表的な例として、Firefox テーマのタブ バー レイアウトの定義とコンパイルを参照してください。
OS とユーザーのオプションのみを考慮しているにも関わらず、約 360 行のファイルのコンパイル結果は約 1400 行に達します。

結論としては、効果的な設計の実装には本質的にスケーラブルが必要であり、通常はプログラムまたは明確に定義されたルールセットを通じて管理される必要があります。
その結果、大規模な一貫した管理のための設計システムが誕生しました。

5. デザインシステムの背景

デザイン システムは、ビジュアル スタイルから UI パターン、コード実装に至るまで、デザインと開発のあらゆる側面をカバーする単一の信頼できる情報源として機能します。

Rethinking CSS in JS

Nielsen Norman Group によると、デザイン システムには次のものが含まれます。

  • スタイル ガイド: ブランド、コンテンツ、ビジュアル デザインなど、特定のスタイルのニーズに関するスタイル ガイダンスを提供するドキュメント。
  • コンポーネント ライブラリ: これらは、ボタンなどの再利用可能な個別の UI 要素を指定します。各 UI 要素について、カスタマイズ可能な属性 (サイズ、コピーなど)、さまざまな状態 (有効、ホバー、フォーカス、無効)、およびそれぞれの再利用可能なクリーンでタイトなコードなどの情報を含む、特定の設計と実装の詳細が提供されます。要素。
  • パターン ライブラリ: これらは、再利用可能なパターン、またはコンポーネント ライブラリから取得された個々の UI 要素のグループを指定します。たとえば、タイトル、パンくずリスト、検索、主ボタンと副ボタンで構成されるページ ヘッダーのパターンが表示される場合があります。
  • 設計リソース: 設計者がコンポーネントとライブラリを実際に使用および設計するには、設計ファイルが必要です (通常は Figma 内)。通常、デザイナーや開発者が使用できるように、ロゴ、書体、フォント、アイコンなどのリソースも含まれています。

デザイン システムは、機能フォームアクセシビリティ、および カスタマイズをサポートし、デザイナーと開発者にとっての交差点として機能する必要があります。
しかし、デザイナーと開発者は考え方が異なり、異なる視点を持っています。

コンポーネントをレンズとして使用して、設計者と開発者の視点の違いを認識しましょう!!

5.1 コンポーネントの構造

デザイナーは、チェックボックス コントロールにどのアイコンを使用するかを決定する必要もあります。
Rethinking CSS in JS

デザイナーは形状に注目する傾向があり、開発者は機能に注目する傾向があります。
デザイナーにとって、ボタンは押したくなるような見た目であればボタンですが、開発者にとっては、押せる限りボタンです。

コンポーネントがより複雑であれば、設計者と開発者の間の溝はさらに広がる可能性があります。

Rethinking CSS in JS

5.2 設計者の考慮事項

  • ビジュアルオプション: プライマリ、アクセント、アウトライン、テキストのみなどの設定されたオプションに従って外観が変わります。
    Rethinking CSS in JS

  • 状態オプション: 状態とコンテキストに応じて外観が変化します
    Rethinking CSS in JS

  • デザインの決定: コンポーネント構造、ビジュアル/状態オプション、ビジュアル属性 (色、タイポグラフィ、アイコンなど) などの値を決定します。

5.3 開発者の考慮事項

  • オプション: 設定可能な初期値。ビジュアルオプションも含まれています。例) アウトライン、サイズ
  • 状態: ユーザーの操作に基づいて変更されます。例) ホバー、押した、フォーカス、選択(チェック)
  • イベント: 状態の変化をトリガーするアクション。例) HoverEvent、PressEvent、FocusEvent、ClickEvent
  • コンテキスト: 動作に影響を与えるコードから挿入される条件。例) 読み取り専用、無効

最終的な形式は、オプション、状態、コンテキストの組み合わせであり、前述の組み合わせ爆発が生じます。

これらのうち、Option は設計者の視点と一致しますが、State と Context は一致しません。
並列状態、階層、ガードなどを考慮して状態圧縮を実行し、デザイナーの視点に戻ります。
Rethinking CSS in JS

  • 有効: 無効オフ、押されたオフ、ホバーされたオフ、フォーカスされたオフ
  • ホバー: オフが無効、オフが押された、ホバーがオン
  • フォーカス中: 無効オフ、押下オフ、フォーカスオン
  • 押すと: 無効 OFF、押すと ON
  • 無効: 無効オン

6. スタイルはどのように管理されていましたか?

もうお気づきかと思いますが、高品質の UI を作成して維持するのは大変な作業です。

さまざまな状態は状態管理ライブラリでカバーされていますが、スタイルはどのように管理されていたのでしょうか?
解決策がまだ確立されていないため、方法論、ライブラリ、フレームワークが次々と登場していますが、3 つの主要なパラダイムがあります。
Rethinking CSS in JS

  1. セマンティック CSS: 要素の目的または意味に基づいてクラスを割り当てます。
  2. Atomic CSS: スタイル (ビジュアル) 属性ごとに 1 つのクラスを作成します。
  3. JS の CSS: JavaScript で記述し、コンポーネント単位で CSS を分離します。

その中でも、JS の CSS は、スタイルの表現と管理に根本的に異なるアプローチを使用するパラダイムのように感じられます。
これは、JS の CSS が メカニズム に似ているのに対し、セマンティック CSS とアトミック CSS は ポリシー に似ているためです。
この違いのため、JS の CSS は他の 2 つのアプローチとは分けて説明する必要があります。 (画像出典)

Rethinking CSS in JS

JS メカニズムでの CSS について議論するとき、CSS のプリ/ポスト プロセッサーが思い浮かぶかもしれません。
同様に、ポリシーについて話すとき、「CSS 方法論」が思い浮かぶかもしれません。

そこで、次の順序でスタイル管理方法を紹介します: JS の CSS、プロセッサ、セマンティック CSS とアトミック CSS、その他のスタイル方法論。

6.1 JS の CSS

では、JSにおけるCSSの正体は何でしょうか?
答えは上記の定義にあります。

JavaScriptで記述し、コンポーネント単位でCSSを分離します。

  1. JavaScript で書かれた CSS
  2. コンポーネントレベルでの CSS 分離

これらの中で、CSS 分離は既存の CSS に十分に適用でき、グローバル名前空間と分離破壊の問題を解決できます。
これは CSS モジュールです。

Rethinking CSS in JS

上記のCSS in JS解析記事へのリンクをもとに、機能を以下のように分類してみました。
各機能にはトレードオフがあり、これらは JS で CSS を作成する際の重要な要素です

6.1.1 統合

特に注目すべきコンテンツは

SSR(サーバーサイドレンダリング) と RSC(React Server Component) です。 これらはフロントエンドを代表するReactやNEXTが目指す方向性であり、実装に大きな影響を与える重要なものです。

  • IDE: 構文の強調表示とコード補完
  • TypeScript: タイプセーフかどうか
  • フレームワーク
    • Agnostic: フレームワークに依存せず、StyledComponent のようなライブラリは React 専用に設計されています。
    • SSR: サーバー上でレンダリングするときにスタイルを文字列として抽出し、ハイドレーションをサポートします
    • RSC: RSC はサーバー上でのみ実行されるため、クライアント側 API は使用できません。
サーバーサイドレンダリングはサーバー上でHTMLを作成してクライアントに送信するため、

文字列として抽出する必要があり、ストリーミングへの応答が必要です。スタイル付きコンポーネントの例と同様に、追加の設定が必要な場合があります。 (画像出典)

Rethinking CSS in JS

  1. Server-side style extraction
    • Should be able to extract styles as strings when rendering on the server
    • Insert extracted styles inline into HTML or create separate stylesheets
  2. Unique class name generation
    • Need a mechanism to generate unique class names to prevent class name conflicts between server and client
  3. Hydration support
    • The client should be able to recognize and reuse styles generated on the server
  4. Asynchronous rendering support
    • Should be able to apply accurate styles even in asynchronous rendering situations due to data fetching, etc.

Server components have more limitations. [1, 2]
Server and client components are separated, and dynamic styling based on props, state, and context is not possible in server components.
It should be able to extract .css files as mentioned below.

Rethinking CSS in JS

  1. Static CSS generation
    • Should be able to generate static CSS at build time
    • Should be able to apply styles without executing JavaScript at runtime
  2. Server component compatibility
    • Should be able to define styles within server components
    • Should not depend on client-side APIs
  3. Style synchronization between client and server
    • Styles generated on the server must be accurately transmitted to the client

6.1.2 Style Writing

As these are widely known issues, I will not make any further mention of them.

  • Co-location: Styles within the same file as the component?
  • Theming: Design token feature supports
  • Definition: Plain CSS string vs Style Objects
  • Nesting
    • Contextual: Utilize parent selectors using &
    • Abitrary: Whether arbitrary deep nesting is possible

6.1.3 Style Output and Apply

The notable point in the CSS output is Atomic CSS.
Styles are split and output according to each CSS property.

Rethinking CSS in JS

  • Style Ouput
    • .css file: Extraction as CSS files