🎉

Next.js App Router対応! CSS-in-JSライブラリ「Molcss」の紹介

2024/01/11に公開

先日、Molcss v0.1.0 をリリースしました。
Next.js App Router に対応できたら紹介記事を書くぞと思っていたので[1]、これを機に紹介したいと思います。

https://www.npmjs.com/package/molcss

Molcss とは?

Molcss は、Near-Zero Runtime CSS-in-JS ライブラリです。ソース中に記述したスタイルをビルド時に CSS ファイルに抽出し、実行時のランタイムコストを最小限に抑えます。そして、Molcss は、自動生成する CSS ファイルに Atomic CSS という手法を採用しています。これを取り入れた CSS-in-JS (Atomic CSS-in-JS) はまだ数は少ないですが、Chakra UIが昨年6月にリリースした Panda や Meta (旧Facebook) が先月12月にオープンソース化[2]した StyleX と、今密かに熱い技術の1つです。

Molcssの使用イメージ
import { css } from 'molcss'

const molcssStyle = css`
  color: red;
  border: 1px solid black;

  &:hover {
    color: blue;
  }
`

const UseInReact = () =>
  <div className={molcssStyle}></div>

Atomic CSS-in-JS とは

Atomic CSS-in-JS は、CSS-in-JS が生成するスタイルに Atomic CSS のデザイン手法を組み込んだものです。

CSS-in-JS についてはすでに皆さんご存知かと思いますが、JavaScript 等のコード中に CSS を書く手法、及びそれを実現するためのライブラリのことです。書いたスタイルを実行時に処理するものやビルド時に事前に CSS ファイルに抽出するもの等実装方法には種類がありますが、処理のイメージとしては下記の通りです。

input.js
const sampleClassName = css`
  color: red;
  padding: 8px;

  &:hover {
    color: blue;
  }
`
output.js
// 上記のコードを Babel 等で変換した際のイメージ
// 変換時に下記の CSS も同時に生成される
const sampleClassName = "m1bdmy7h"
output.css
.m1bdmy7h {
  color: red;
  padding: 8px;
}
.m1bdmy7h:hover {
  color: blue;
}

input.js 中に書いた CSS は output.css に抽出され、コードには代わりにそのクラス名が記載されます。
CSS-in-JS ではクラス名はビルド時に機械的につけられるため、開発者はクラス名を重複しないように命名する必要がなくなるメリットがあります。

一方の Atomic CSS は、スタイルを Atomic (これ以上分解できない単位) に分割し、それらを組み合わせてコンポーネントを構築するスタイリング手法です。ほぼ同じ概念に「ユーティリティファースト」があり、Tailwind CSS がこれを採用しています。こちらのほうが有名なのでイメージしやすいかもしれません。

const Component = () =>
  <div className="text-[red] p-[8px] hover:text-[blue]">

上記のコードがあると、Tailwind CSS は次の CSS を出力します。

.text-\[red\] {
  color: red;
}
.p-\[8px\] {
  padding: 8px;
}
.hover\:text-\[blue\]:hover {
  color: blue;
}

一般的な CSS 設計からするとアンチパターンに見えるこの CSS ですが、実はこれは効率的なコードです。他のコンポーネントで padding: 8px なスタイルを使用したい場合は p-[8px] を使用します。その際、CSS の増加はありません。定義済みのスタイルが再利用されます。
小規模な Web サイトの場合は普通に書くより CSS ファイルサイズが増えるかもしれませんが、ある一定以上の規模になれば大幅なファイルサイズ削減となるでしょう。

そして Atomic CSS-in-JS はこれら2つの特性を併せ持ちます。
つまり、このコードから →

input.js
const sampleClassName = css`
  color: red;
  padding: 8px;

  &:hover {
    color: blue;
  }
`

これらのコードを吐き出します。

output.js
const sampleClassName = "text-[red] p-[8px] hover:text-[blue]"
output.css
.text-\[red\] {
  color: red;
}
.p-\[8px\] {
  padding: 8px;
}
.hover\:text-\[blue\]:hover {
  color: blue;
}

開発者はいつも通りスタイルを書くだけで、裏では Atomic CSS-in-JS ライブラリが自動で CSS ファイルサイズ増加を抑えてくれるのです。

改めて Molcss の紹介

Atomic CSS-in-JS の魅力をお伝えしたところで、改めて Molcss について説明します。

生成されるクラス名が短い

Atomic CSS(-in-JS) はその性質上、1つの要素に多くのクラス名を指定します。いくら CSS のファイルサイズ増加を抑えたところで、HTML(SSR をしている場合)が肥大化してしまいます。
Molcss では、クラス名が最小2文字のため、ファイルサイズは他の Atomic CSS-in-JS より軽くなります。このライブラリ一番のアピールポイントです。

SSR / SSG 後の HTML 例
<header class="v0 C0 a1 h0 bQ0">
  <div class="i0 k0 a2 d0">
    <a class="s0 f0 c0 Z0" href="/molcss/">molcss</a>
    <nav class="i0">
      <ul class="i0 bR0 Y0 an0 a3 d1">
        <li class="a3 d1">
          <a class="c0 Z0 w0 u0 w1a" href="https://github.com/iMasanari/molcss" target="_blank"rel="noopener">GitHub</a>
        </li>
      </ul>
    </nav>
  </div>
</header>

ちなみに、上記は作成途中の Molcss サイト の内容です。

スタイルのハイブリッドアプローチ

Molcss は原則、ビルド時にすべてのスタイルを抽出して実行時にはゼロランタイム CSS-in-JS として動作します。しかし、css タグに変数を埋め込んだ場合(ex. css`color: ${value}`)はランタイム CSS-in-JS として実行時にスタイルを生成することがあります。
これは、インラインスタイルで CSS 変数を渡す方式に比べてパフォーマンスの懸念はありますが、コンポーネントには className prop を渡すだけで良いというメリットがあります(style prop 等を開ける必要はありません)。

/** @jsxImportSource molcss/react */
import { css } from 'molcss'
import { renderToString as render } from 'react-dom/server'

const staticStyle = css`
  color: blue;
`

const value = 'red'

const dynamicStyle = css`
  color: ${value};
`

const Component = ({ className, children }) =>
  <div className={className}>{children}</div>

/* 変数埋め込みがない場合は、いつも通り */
console.log(render(<div className={staticStyle}>static style</div>))
// -> `<div class="c0">static style</div>`

/* 変数埋め込みがあっても HTML Element であれば、インラインスタイルを使用してゼロランタイムで動作する */
console.log(render(<div css={dynamicStyle}>HTML element</div>))
// -> `<div class="c1" style="--molcss-bL:red;">HTML element</div>`

/* 変数埋め込みがあるスタイルがコンポーネントに指定された場合、ランタイム CSS を生成する(下記出力は SSR 時の内容) */
console.log(render(<Component css={dynamicStyle}>Component</div>))
// -> `<style data-molcss="bL1621153576">.bL1621153576{--molcss-bL:red}</style>
//     <div class="c1 bL1621153576">Component</div>`
生成される CSS
.c0 { color: blue }
.c1 { color: var(--molcss-bL) }

タグつきテンプレートリテラルによるスタイルの記述

CSS-in-JS ライブラリの中にはオブジェクト形式でスタイルを記載するものもありますが、Molcssでは css タグのテンプレートリテラルに記載します。どちらも一長一短ではありますが、アピールポイントの1つです。

私は「CSS-in-JS における CSS 記述は DSL の一種である」と考えているので、Molcss ではこちらを採用しました。
css タグであれば、素の CSS をほぼそのまま活用できるほか、前述のランタイム時の値が入っていることがわかりやすいです。

const color = 'blue'

// Molcss では、${...} があるかないかで型が変わる
const staticStyle = css`
  color: red;
`
console.log(typeof staticStyle) // 'string'

const dynamicStyle = css`
  color: ${color};
`
console.log(typeof dynamicStyle) // 'object'

const className = generateRuntime(dynamicStyle) // Vanilla JS であれば `generateRuntime` 関数、
const App = () => <div css={dynamicStyle} />    // React であれば `css` prop でクラス名として使用できるようになる

上記の通り、color: ${value}; のような変数の埋め込みがある場合にあえてオブジェクト型に変更している[3]のですが、TypeScript の型もちゃんとオブジェクト型(RuntimeStyle)で推論されます。
これはタグ付きテンプレートリテラルならではの恩恵です。

Molcss with Next.js の設定方法

前章では、Molcss の良さについて、紹介してきました。
この章では、実際に Molcss を使うための方法をお伝えいたします。以下は、構築済みの Next.js 環境に Molcss を導入するための手順です。

まずは、npm から Molcss をインストールします。また、postcss をカスタマイズする際はプラグインを再インストールする必要があるので、こちらも入れておきます。
なお、yarn 等他のパッケージマネージャーを使用している場合はそちらでインストールしても構いません。

npm install molcss
npm install postcss-flexbugs-fixes postcss-preset-env

Molcss ではコード変換に JavaScript用の babel-plugin と CSS 生成用の postcss-plugin の2つのプラグインを使用するのですが、それらプラグイン間でデータを共有する必要があります。それを行うためのファイルを作成します。

// molcss.context.js
const { createContext } = require('molcss/context')

module.exports = createContext()

次に、babel-pluginpostcss-plugin の設定ファイルを作成します。

// babel.config.js
const molcssContext = require('./molcss.context')

module.exports = {
  presets: [
    ['next/babel', {
      'preset-react': {
        runtime: 'automatic',
        importSource: 'molcss/react',
      },
    }],
  ],
  plugins: [
    ['molcss/babel-plugin', {
      context: molcssContext,
    }],
  ],
}
// postcss.config.js
const molcssContext = require('./molcss.context')

module.exports = {
  plugins: [
    // nextjs default settings
    // https://nextjs.org/docs/pages/building-your-application/configuring/post-css#customizing-plugins
    'postcss-flexbugs-fixes',
    ['postcss-preset-env', {
      autoprefixer: {
        flexbox: 'no-2009',
      },
      stage: 3,
      features: {
        'custom-properties': false,
      },
    }],
    // molcss settings
    ['molcss/postcss-plugin', {
      content: [
        'app/**/*.{js,jsx,ts,tsx}',
        'src/**/*.{js,jsx,ts,tsx}',
      ],
      context: molcssContext,
    }],
  ],
}

注意点は下記の2つです。

  • それぞれのプラグイン設定時に、context に上記でエクスポートしたオブジェクトを指定すること
  • コンポーネント等、Molcss でスタイルを記載するコードは postcss-plugin に記載する content のglobで指定しておくこと

最後に、app/layout.tsx に下記をのコードを記載すれば Molcss の準備が完了です。

import 'molcss/style.css'

あとは import { css } from 'molcss' して、スタイルを記載していきます。
ちなみに Molcss は、Next.js のサーバーコンポーネント中でも使用できます。
※サーバーコンポーネント中に動的スタイルがある場合は、裏側でクライアントコンポーネントが呼び出されます(とはいえ開発者が意識する必要は特にありません)。

なお、バンドラーに Vite を使用する際のセットアップ方法は、GitHub の README.md を参照してください。

おまけ: webpack-plugin について

Molcss v0.0.X までは babel と postcss ではなく、webpack-plugin で Next.js (仮)対応していました。しかし仮想ファイルと Next.js のキャッシュの相性が悪く、dev サーバーが安定しませんでした(私の Atomic CSS-in-JS 実装方法では、仮想ファイルが必須でした)。そんな中、CSS 生成を仮想ファイルではなく postcss 側で行うという方法にたどり着き、なんかうまく行ってそうだぞというのがこの v0.1.0 です。
とはいえ、設定の手軽さは webpack-plugin(及びそれを Next.js 用にラップした関数の用意) なので、今回の結果をもとに、再び webpack-plugin 対応にチャレンジしたいです。

最後に

Molcss は無事 Next.js 対応を完了しましたが、これで完成ではありません。さらなる機能強化、APIのさらなる洗練が必要です。
この記事を読んで少しでも良いなと思った方は、いいねやコメント、GitHub のスターをいただけますと励みになります。

https://github.com/iMasanari/molcss

あと、開発中にあたってのメモ書きや Molcss での Atomic CSS-in-JS 実装方法等は下記のスクラップにまとめています。もしよろしければ、こちらも覗いてみてください。

https://zenn.dev/imasanari/scraps/d00c333a4d530a

脚注
  1. Molcss v0.0.X でも一応 Next.js で動きますが、molcss が生成した CSS の更新が反映されなくなることがありました。そうなったら .next/cache を削除して dev サーバーを再起動する必要があります。なので、v0.0.X ではメインは Vite とし、Next.js を Experimental としてサポートしていました。 ↩︎

  2. StyleX の存在自体は、React Conf 2019 で発表されています。ちなみに Molcss は、StyleX が「1年後にオープンソース化する」という情報から1年以上経っても公開されないなら自分で作ってみるか! と思ったことがきっかけで開発をスタートしました。結果的に StyleX オープンソース化の方が先になってしまったのですが、Molcss も良いライブラリになっていますので、ぜひ使っていただければと思います。 ↩︎

  3. Reactの最適化のため。useInsertionEffect でスタイル注入します。 ↩︎

Discussion