🎃

Atomic CSSとTailwindCSSの考え方

2024/04/15に公開

概要

まだ批判的な人も多いようですか、昨今間違いなく流行りの兆しを見せているTailwindCSS。

なぜTailwindCSSがここまで使われるようになり始め、その設計思想はどのようなものなのか、簡単に従来のスタイルアプローチと比較しながら調べてみました。

Semantic CSS

Semantic CSS(セマンティックCSS)は、HTMLの要素に意味のある、記述的なクラス名を使用してスタイリングする古典的アプローチです。この設計思想は、クラス名がその要素の内容や機能を直接的に反映するように命名されるべきだという考えに基づいています。

例えば次のようのHTMLを考えてみましょう。

<div class="article-preview">
  <img class="article-preview__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="article-preview__content">
    <h2 class="article-preview__title">Stubbing Eloquent Relations for Faster Tests</h2>
    <p class="article-preview__body">
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>

出典 https://adamwathan.me/css-utility-classes-and-separation-of-concerns/
このHTMLから読み取れるのは、このHTMLは記事のプレビューを表示を担う箇所であり、中には画像やタイトル、本文などが入っているということです。当然レイアウトは何もないので、これに対してスタイルを当てる必要があります。このHTMLに対して、スタイルを当ててみましょう。

.article-preview {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.article-preview__image {
  display: block;
  width: 100%;
  height: auto;
}
.article-preview__content {
  padding: 1rem;
}
.article-preview__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.article-preview__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

問題点

これでスタイルも当てれたのでめでたしめでたし・・・とはいきたくないところです。
上記のスタイルの作り方にはいくつか問題点があります。

HTMLへの密依存

まず、 article-preview__contentarticle-preview__title といったコンテンツの要素に対して当てるスタイルについて、そもそもの話として article-preview で指定したスタイルが当たっているということが前提となっており、そしてその前提を保証するのはCSSではなくHTMLであるいう問題があります。万が一HTMLを変更する場合は当然CSSも変更する必要が出てきます。またデグレの温床になり得るでしょう。

汎用性の低さ

このプレビューと全く同じようなスタイルだが、場所によって微妙に異なるものにしたい、といった場合はどうするかという問題もあります。(全部コピペして微妙に変える、SCSSなどではmixinなどを使って共通化するアプローチが取られているかと思います)

クラス数と記述量の肥大化

上記と関連してですが、共通化できなければ当然似たようなクラスが増えていくことになります。
ページや要素が増えていけばいくほどクラスが量産されていき、保守性がどんどん下がっていくことになるでしょう。

上記のような問題点がありながらも、セマンティックなCSSのアプローチというのは、やはりHTMLと紐づいているためどこに対してのスタイルなのかわかりやすいという点で検索性が高く、また直感的なスタイルアプローチであるため広く使われていたように思います。
そのためJSでのWebページの構築が主流となった現在でもCSS ModulesといったセマンティックなCSSのアプローチと相性の良い手段が提供されてきました。

CSS-in-JS

現代ではReactやVueといったコンポネント指向のライブラリが覇権を握り、コンポネントを利用したページ・UIの構築といったものが完全に主流となりました。そこで新たに登場してきたスタイルアプローチがCSS-in-JSです。HTMLは基本的にUIをパーツに分割するといったことには対応しておらず(現在はWeb Componentsなどが登場してきていますが)、JSやCSSはグローバルに宣言する必要がありました。

しかしこうしたコンポネント指向の登場によって、レイアウトをパーツごとに分解し、それぞれに対してロジックやスタイルを定義する、といった設計が可能となったため、スタイルの定義も大きく変わることになりました。

先ほどのHTMLとCSSを少し改変してCSS-in-JSのライブラリであるemotionで再現してみると以下のような形です。

import React from 'react';
import styled from '@emotion/styled';
import { css, ThemeProvider } from '@emotion/react';

const dynamicStyle = ({
  variant,
  color,
  textAlign,
  lineHeight,
  marginTop,
  marginBottom,
}) => css`
  color: ${theme => theme.colors.text[color]};
  font-size: ${theme => theme.typography.paragraph[variant].fontSize};
  text-align: ${textAlign};
  line-height: ${lineHeight};
  margin-top: ${theme => theme.spacing[marginTop]};
  margin-bottom: ${theme => theme.spacing[marginBottom]};
`;

const Article = styled.div`
  background-color: white;
  border: 1px solid hsl(0, 0%, 85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  padding: 1rem;
`;

const Title = styled.h2`
  ${props => dynamicStyle({
    variant: 'large',
    color: 'primary',
    textAlign: 'center',
    lineHeight: '1.3',
    marginTop: 'medium',
    marginBottom: 'small',
  })}
`;

const Body = styled.p`
  ${props => dynamicStyle({
    variant: 'medium',
    color: 'secondary',
    textAlign: 'left',
    lineHeight: '1.5',
    marginTop: 'small',
    marginBottom: 'medium',
  })}
`;

const ArticleComponent = ({ title, body, theme }) => {
  return (
    <ThemeProvider theme={theme}>
      <Article>
        <Title>{title}</Title>
        <Body>{body}</Body>
      </Article>
    </ThemeProvider>
  );
};

JSでスタイルを管理するため、引数に応じてスタイルを変更させるといったことも可能になり、従来のセマンティックなCSSアプローチで欠点としてあった汎用性については克服したように思います。

また、スタイルについてもグローバルに宣言しなくなったことで、スコープをコンポネント配下に限定できるようになったり、class名を個別で考える必要がなくなったり(当然コンポネント名は考える必要がありますが、、)と多くのメリットを享受できるようになりました。

問題点

しかし柔軟さと汎用さを確保できるようになった一方、周辺技術の進化や発展に伴いやはりこのアプローチ(というかJSでスタイルを制御すること自体)にも新たに問題提起されるようになっていきます。

スタイルの再計算に対するオーバーヘッド

多くのCSS-in-JSライブラリではJSで管理する状態に応じてブラウザのStyleRuleを更新します。
しかしこの更新が起きるたびに、Render Treeの算出が再実行され再描写等のブラウザの処理が実行されてしまいます。ReactにおけるConcurrent Renderingの登場等によってこのあたりのパフォーマンスについて改めて問題視されるようになりました。

https://dev.to/gopal1996/understanding-reflow-and-repaint-in-the-browser-1jbg

スタイルの事前作成の困難さ

ブラウザ上で実行するJSが管理する状態に応じてスタイルを変えるということは、逆を言えば利用するスタイルを事前に決定することができないということを意味します。(スタイルアプローチ自体についての欠点からは若干逸れますが)
ライブラリやフレームワークによってはバンドラーやそのプラグインなどでこの問題を回避しようとしているものもありますが、やはり事前に決定するにはある程度の限界は存在します。SSRやSSGを利用する環境などでは、ライブラリによってはこの問題はボトルネックになる場合があるかと思います。
またCSS部分についてホスティングでキャッシュしておく、といったことも当然できないためCSSファイルを利用する場合よりやはりパフォーマンスで後手に回ります。

Atomic CSS/Functional CSS/Utility First CSS

Atomic CSS(Functional CSS/Utility First CSS)とは、スタイルシートの設計方法の一つで、多くの小さな、単一目的のクラスを作成していくCSSの設計思想です。

これまで紹介してきたアプローチの問題点や周辺技術の進化・流行から最近注目を浴び始めているスタイルアプローチになってきているように思います。

各クラスは1つの特定のスタイルプロパティ(例えば、マージン、パディング、フォントサイズ)を設定します。このアプローチの主な利点は、コードの再利用が容易であること、スタイルシートのサイズが最小限に抑えられること、そしてスタイルの整合性が保たれることです。

例えば以下のようなスタイルルールがAtomic CSSに該当します。

.absolute {
	display: absolute;
}
.flex {
	display: flex;
}
.text-center {
	text-align: center;
}

Atomic CSSは、Tailwind CSSやTachyonsのようなフレームワークで広く採用されています。これらのフレームワークは、開発者がクラス名を組み合わせることで、迅速にレイアウトを作成できるように設計されています。

インラインスタイルとの違い

しばしば、Utility Firstなスタイル設計は、インラインスタイルで記載するのと変わらないのではと言われることがあるようですが、二つは全く異なるものといってよいでしょう。

インラインスタイルでは設定できる値に対する制約が一切なく、デザインシステムを保証することが難しいのに加え、直接マークアップすることによるキャッシュの効率が低下する懸念などがあります。

利点

利用するスタイルの事前決定が可能

このアプローチで定義されるスタイルは基本的に全てがUtilityクラスになります。
そのため、CSS側ではすでに利用するスタイルは定義し終えている前提であるため、ビルドの段階で利用するスタイルを全て決定することができます。CSSファイルとして提供することが可能となるため、キャッシュ戦略によってパフォーマンスの改善が図れます。
また、HTML(JS)側でclass名を指定するだけで済むため、SSRやSSGを利用する環境でもスタイルの問題がボトルネックとなりえません。

スタイルルール数の肥大化を避けられる

新たなコンポネントを追加しようとなっても、HTML(JS)側ですでに定義されているスタイルを組み合わせて構築することができるため、セマンティックアプローチのようにコンポネントが増えるごとにクラス数が肥大化するといったことを避けやすいといった利点があります。

https://johnpolacek.medium.com/by-the-numbers-a-year-and-half-with-atomic-css-39d75b1263b4

古典的なセマンティックCSSを利用していた場合からAtomic CSSを利用するようリファクタした場合におけるCSSファイルのサイズ推移を計測された方がいらっしゃいます。
やはりプロジェクトが発展していくごとにスタイル数は肥大がしていく傾向がありますが、Atomic CSSアプローチに切り替えたタイミングで大幅な削減が行えたようです。
出典: By The Numbers: A Year and Half with Atomic CSS

同じようなアプローチを持つライブラリなどは多く存在していたかと思います。(例えばBootstrapなど)
なぜ今この時代にAtomic CSSがまた流行り始めているかというと、やはりコンポーネント指向がメジャーになったことで再利用性を容易に確保できるようになったこと、そしてSSRやSSGといったサーバー側でDOMを取り扱うことが増えクライアント側でスタイルを生成するニーズが落ちてきている、といった事情が大きいかと思います。

TailwindCSS

Atomic CSSを採用している代表的なCSSフレームワークとして、今最も知名度があるであろうものはTailwindCSSでしょう。

Tailwind CSSは、Adam Wathan氏が開発したユーティリティファーストのCSSフレームワークです。頻繁に使用されるスタイル属性に対応する数多くのユーティリティクラスを提供しており、デベロッパーはHTML内に直接クラスを適用することで迅速にスタイリングを行うことができます。

レスポンシブや擬似クラス、 テーマ設定やプラグインシステムなども備えており、モダンなUIデザインを実現するために必要な機能を一通り用意されているフレームワークです。

前述までに出した例をTailwindCSSとHTMLで書き換えたものになります。

<div class="max-w-xl mx-auto overflow-hidden bg-white border rounded-lg border-gray-200 shadow-md">
    <img class="w-full" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
    <div class="p-5">
        <h2 class="text-xl font-semibold text-gray-800">Stubbing Eloquent Relations for Faster Tests</h2>
        <p class="mt-2 text-base text-gray-600">
            In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
        </p>
    </div>
</div>

HTML側であらかじめ存在するUtilityクラスを利用してスタイルを組み立ています。

この例ではUtilityクラスだけで組み立てられているため、スタイルを別途宣言する手間を省けています。

セレクタ

TailwindCSSは単にclassのプリセットを提供するだけでなく、セレクタを利用したスタイルなども簡単に実装できるようになっています。(この辺がFunctinal CSSの由来かもしれません)
例えば下記のような定義のHTML(JS)が存在した場合、

<div className="mt-6 flex flex-col gap-2 group">hover area</div>
<div className="group-hover:bg-emphasis">this will be black</div>

ビルド後には次のようなCSSが構築されます。

.group:hover .group-hover\:bg-black {
  --tw-bg-opacity: 1;
  background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}

このようにセレクタを利用したスタイルの構築も抽象化してくれるのがTailwindCSS特徴の一つです。

カスタマイズ性

プロジェクトルートに配置する設定ファイルtailwind.config.js に追記するだけで、プラグインやカスタムクラス、デザインテーマなどを指定することができます。

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
    './src/features/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    /**
     * @see https://v1.tailwindcss.com/docs/border-radius
     */
    borderRadius: {
      md: '6px',
      xl: '12px',
      '5xl': '60px',
    },
    borderWidth: {
      '1': '1px',
    },
    colors: {
      emphasis: '#D84190',
      error: '#E50000',
      black: '#000000',
      white: '#FFFFFF',
      inherit: 'inherit',
    },
    ~

より踏み込んだ使い方もでき、例えばvariantを追加してある程度のOS差分をCSS吸収する、といったことも可能です。

plugins: [
    plugin(({ addBase, addComponents, addUtilities, addVariant, theme }) => {
			// モバイル対応
      addVariant("mobile", "@media screen and (pointer:coarse)");
      addVariant("not-mobile", "@media not screen and (pointer:coarse)");
      // safari対応
      addVariant(
        "safari",
        "@media screen and (-webkit-min-device-pixel-ratio:0)"
      );
      // iOS対応
      addVariant(
        "ios",
        "@media screen and (-webkit-min-device-pixel-ratio:0) and (pointer:coarse)"
      );
    })
~
<div
    className={clsx(
      "fixed z-second top-header right-0",
      // iOSの場合のみ適用する
      "ios:bottom-0 ios:right-0"
    )}
  >
~

スタイルの再利用

ベースの思想として前述したUtility Firstがあるため、スタイルを共通化したいと言って安易に独自のカスタムクラスを生成するのはアンチパターンとなります。

TailwindCSSでは基本的にはコンポネント指向のフレームワーク(※1)と併用することで、共通のスタイルではなく共通のコンポネントを作ることを推奨しているようです。コンポネントを利用することで、独自のスタイルルールを増殖させることなくプロジェクトのデザインを実装することが可能となります。下記がコンポネントと併用した例となります。

import React from 'react';

// 画像表示用コンポーネント
const ArticleImage = ({ src, alt }) => {
    return <img className="w-full" src={src} alt={alt} />;
}

// タイトル表示用コンポーネント
const ArticleTitle = ({ children }) => {
    return <h2 className="text-xl font-semibold text-gray-800">{children}</h2>;
}

// 本文表示用コンポーネント
const ArticleBody = ({ children }) => {
    return <p className="text-base text-gray-600">{children}</p>;
}

// 記事プレビューコンポーネント
const ArticlePreview = ({ imageUrl, title, body }) => {
    return (
        <div className="max-w-xl mx-auto overflow-hidden bg-white border rounded-lg border-gray-200 shadow-md">
            <ArticleImage src={imageUrl} alt={title} />
            <div className="p-5 flex flex-col gap-2">
                <ArticleTitle>{title}</ArticleTitle>
                <ArticleBody>{body}</ArticleBody>
            </div>
        </div>
    );
}

export default ArticlePreview

今回は結構ドメイン寄りのコンポネントを作成していますが、より汎用的なデザインコンポネントとして切り出したりすることで、独自のクラスを作らずにデザインルールに沿った画面を構築していくことが可能になります。

とは言ってもMVCモデル型のフレームワークやテンプレート型のフレームワークを利用する場合など、再利用可能なUIを作るコストが高い場合が想定されるでしょう。そのためTailwindCSSでは、ベースとなるグローバルのCSSファイルに @apply ディレクティブを追記することでカスタムクラスを作る選択肢が用意されています。

Reusing Styles - Tailwind CSS

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-primary {
    @apply py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75;
  }
}

上記では btn-primary クラスが作成され、いずれの箇所でも共通のクラスとして利用が可能となります。(※2)

ただ繰り返しになりますが、 @apply を利用することはTailwindCSSの利点(Utility Firstであることによって得られるシンプルさや保守性など)を殺しかねないとして全く推奨されていないことは留意する必要がありそうです。

※1 WebComponentは後述する理由でまだ併用が難しそう。
※2 tailwind.config.js 内で設定することも可能

TailwindCSSへの批判

TailwindCSSにはまだまだ根強い批判もあり、いくつかの懸念が指摘されています。

HTMLが汚い
HTMLに直接classを記述いく形であるため、何も考えずそのままclassを当てていくと行数が長くなったり、もはやどこの部分で何をやっているのかわからなくなる懸念は確かにあります。
これもスタイルの再利用で紹介したように、閉じたコンポネントを細かく切っていくことでどのコンポネントが画面上の何を担っているのか、わかりやすい形で設計するなどの対策が取れそうです。

ShadowDOMやWebComponentsを考慮していない
TailwindCSSでは、ビルド後は基本的に一つのCSSだけが作成され、それを全てのページで参照する形になります。そのためスタイルのスコープが断絶するShadowDom内ではTailwindで定義されている全てのスタイルルールが利用できないことになります。
この点についてはコミュニティでもいくつか議論があるようで、対策についていくつか提案がありました。例えば、

  • twindのようなTailwind互換のランタイムを導入し、WebComponent内で利用する
  • ShadowDOM内でtailwindのCSSをimportする
    • ただ、styleを独立させるのがShadowDOMの目的なので、同じものをimportするとShadowDOMを利用する意味がない

今後の発展次第では、また違う解決策が出てくるかもしれません。

参考記事

https://tailwindcss.com/docs/utility-first
https://johnpolacek.medium.com/by-the-numbers-a-year-and-half-with-atomic-css-39d75b1263b4
https://frontstuff.io/no-utility-classes-arent-the-same-as-inline-styles
https://johnpolacek.github.io/the-case-for-atomic-css/
https://adamwathan.me/css-utility-classes-and-separation-of-concerns/
https://coliss.com/articles/build-websites/operation/css/why-tailwind-css-is-not-for-me.html

Discussion