Molcss: オリジナル Atomic CSS-in-JS の作成メモ
オリジナルな Atomic CSS-in-JS を作成するためのメモ書き。
理想のスタイル定義を求めて自作します。
コメント・いいね等歓迎です。筆者のやる気になります。
作成中でバグ等あるかもしれませんが、npmに公開しています。下記コマンドでインストール可能です。
npm install molcss
今のところ、viteにのみ対応しています。Webpackは対応中です。
Webpackにも対応しました。
仕様
- ゼロランタイムなCSS-in-JS
- ランタイムCSSは今回のスコープ外
- 重複するCSS定義は内部で積極的に再利用する(AtomicなCSS)
- スタイルのマージが可能
記述方法と動作イメージ
Babel変換前
import { css, mergeStyle } from '...'
const a = css`
color: black;
padding: 1px;
margin-top: 4px;
`
const b = css`
color: black;
padding: 2px;
margin: 3px;
`
const c1 = mergeStyle(a, b)
const c2 = mergeStyle(b, a)
Babel変換後
/*
generated css
.c0 { color: black; }
.p1 { padding: 1px; }
.p2 { padding: 2px; }
.m3 { margin: 3px; }
.mt4 { margin-top: 4px; }
*/
import { mergeStyle } from '...'
const a = 'c0 p1 mt4'
const b = 'c0 p2 m3'
const c1 = mergeStyle(a, b) // 'c0 p2 m3'
const c2 = mergeStyle(b, a) // 'c0 p1 m3 mt4'
成果物
-
クライアント用スクリプト (
css
,mergeClass
をエクスポート) -
[ ] バンドラー用プラグイン (まずはviteプラグインを想定)[x] vite[ ] webpack
-
[ ] babelプラグイン(バンドラー用プラグインがない場合のfallback用)- → babelプラグインとpostcssプラグインの合せ技で各バンドラーに対応する
最近作成したtw-tag
はある意味でプロトタイプ。Tailwind CSSをCSS in JS風に記載するライブラリ。
違い:
-
tw-tag
はCSSではなくTailwind CSSのクラス名で記述する- ↔ CSS in JSならクラス名を覚える必要がない
-
tw-tag
はTailwind CSSを使用するため、(ビルド後の)クラス名が人間にとって読みやすい- ↔ CSS in JSならビルド時にクラス名が決定されるため、クラス名を短くできる
Atomic CSS in JS固有の課題とその解決案
詳細度が同じスタイル定義がある場合、記載順によって適用されるスタイルが変わる可能性がある。Atomic CSS in JSの場合、スタイル定義を積極的に再利用するため発生しやすい。
例)
- メディアクエリの違い
- 擬似クラスの違い
- shorthandによる記載方法の違い
メディアクエリの違い
configファイルやオプション等で、使用するメディアクエリの値と順番を指定する。
// config
{ media: ['sm', 'md', 'lg'] }
// component
const style = css`
/* ... */
@media (--sm) { /* ... */ }
@media (--md) { /* ... */ }
`
TODO: css`...`
内で順番を破った場合の順番をどうするか(configの順か、css`...`
記載順か)
擬似クラスの違い
&:hover
と&:active
、どちらを優先するか。
→ いったん、セレクターを辞書順ソートで決める方針で
shorthandによる記載方法の違い
案1: longhandに展開し、そのクラス名を使用する
css`padding: 1px; padding-top: 2px;` // 'pr1 pb1 pl1 pt2'
欠点:
- クラス名が長くなる
- shorthandの展開ロジックが必要
案2: CSS出力順などでカバーする
CSS出力順を、shorthand → longhandの順でソート出力するようにすれば、スタイルが混ざることはない。
スタイル定義をマージする際は、shorthandなクラス名がある場合、出現済みのlonghandを無効化(上書き)することで対応可能。
欠点:
- マージ処理が複雑になる
案2で進める予定
上述の課題など残作業はたくさんあるが、いったんそれっぽい感じには出来てきている。
<header class="js-header bv1 bw1 bq3 Q2 bA0 bk1 bl0 bj0 bC0">
<div class="bn1 y4 v0 bp0 bB0">
<div class="aR1">
<div class="av0 aw0"><a href="/" class="bh0a">Tech SANDBOX</a></div>
<p class="av1 aw1 bm3">技術ブログ改め、Qiitaの下書き</p>
</div>
<a href="https://qiita.com/iMasanari" target="_blank" rel="noopener"
class="be0 bb0 bc0 bd0 aQ0 g0 Q0 bo0 bk0 bl0 bj1 bo1b">
Qiita
</a>
<a href="https://github.com/iMasanari" target="_blank" rel="noopener"
class="be0 bb0 bc0 bd0 aQ0 g0 Q0 bo0 bk0 bl0 bj1 bo1b">
GitHub
</a>
</div>
</header>
【完了】ランタイムCSS案
/** @jsxImportSource molcss/react */
const dynamicStyle = (value: string) => css`
color: ${value};
`
const Component = () =>
<div css={dynamicStyle('red')} />
上記の形式で実装済み。
以下は、対応時のメモ書き。
今回のライブラリではビルド時(babel処理時)に最適化を行うため、実行時のCSS生成(emotionでいうcss`padding: ${num}px;`
のような変数埋め込み)のサポート予定はない。
が、もしサポートするならという形で、案を書いていく。
記載方法
const Foo = () =>
<div className={runtimeStyle({ transform: `translate(${x}px, ${y}px)` })} />
// staticなスタイルと合わせる場合、`mergeStyle`で合成する
mergeStyle(css`/* ... */`, runtimeStyle({ /* ... */ }))
css
関数には手を加えない。
実装方法案
「Atomic CSS in JS固有の課題とその解決案」で記載した通り、CSSの記載順が重要である。つまり、ランタイムでCSSを追加して、期待通りにそのCSSを反映させるのは難しい。そのため、CSS変数を使用して下記の2段階で実装を行う。
- runtimeなCSSにはCSS変数の定義を行う
- staticなCSSにはそのCSS変数を使用したプロパティを記述する
例えば、下記のようなコードの場合、
runtimeStyle({ color: 'red' })
runtimeなCSSには--runtime-color: red;
が、staticなCSSにはcolor: var(--runtime-color);
が定義される。これによって、runtime時に生成されるCSSがどこに挿入されようとも、CSS記載順による影響は受けない。
あとは、mergeStyle
がいい感じにやってくれる(はず)。
@emotion/react
のように、jsxにcss
プロパティを記載する場合に面白くなりそう。
-
Foo
のようなユーザー定義コンポーネントの場合: runtimeなCSSにCSS変数を追加 -
div
のような組み込みコンポーネントの場合: インラインスタイルにCSS変数を定義
jsxImportSource
を使用してcss
プロパティ内にスタイルを書いてもらえば、上記の2パターンを自動で切り分けれるはず。
インラインスタイルに記述した場合、(たぶん)CSSOMの再構築は行われなくなりパフォーマンスが向上する。
とはいえReact専用ライブラリにする予定はないので、案だけ書き残して供養。
css`padding: ${num}px;`
のような形式もサポートするように対応中。
Reactの場合、このような感じにする予定。
export default ({ num }) => {
const runtimeValue = useRuntimeValue()
const style = css`
padding: ${runtimeValue(`${num}px`)};
`
return <div className={style} />
}
useRuntimeValue
を挟むことで、Reactのレンダリング(useInsertionEffect
)に適応させる。
関数オーバーライドすれば、埋め込みありなしで返り値の型を変更できた。
useRuntimeValue
ではなく、型の違いで React 対応しても良さそう。
interface CssTagFunction {
(template: TemplateStringsArray): string
(template: TemplateStringsArray, ...substitutions: string[]): (ctx: RuntimeStyleContext) => string
}
const staticStyle = css`color: red;` // type: string
let foo = 'blue'
const runtimeStyle = css`color: ${foo}` // type: function
実装完了。
staticなクラスはこれまで通り className
propに、dynamicなクラスは css
propに記載可能。
/** @jsxImportSource molcss/react */
const dynamicStyle = (value: string) => css`
color: ${value};
`
const Component = () =>
<div css={dynamicStyle('red')} />
作成中だけど一通り機能は出来たのでnpmで公開。パッケージ名はmolcss
。
下記のコマンドでインストール可能。
npm install molcss
ブログにも反映完了。
ただし、vite-pluginではAstroには対応しきれなかった。(Astro側のバグな気がする)
-
dev
モードの場合CSS読み込みで404エラーが発生する(スタイルは反映される)- CSSは
<style>
埋め込みと<link>
参照で(なぜか)2回読み込まれるが、molcss
のCSSは仮想モジュールなので<link>
参照で404エラー
- CSSは
- Astroがv2.7.0であれば
astro build
は通るが、執筆時最新のv2.10.1ではエラーでビルドできない
だが、Tailwind CSSを使用していたときと比べてクラス名が短くなり、HTML / CSSが軽量化した。
いっぽう、VS Codeでcss`...`
のシンタックスハイライトを行うvscode-styled-components
はJavaScript、TypeScriptの拡張子のみの対応のため、.astro
はハイライトされなかった。つらい。
devモードの場合CSS読み込みで404エラーが発生する(スタイルは反映される)
力技で解決。
// NOTE: Astro requests path with `.../�virtual:molcss/style.css`
if (importee.endsWith(`/�${STYLE_PATH}`)) {
return `\0${STYLE_PATH}`
}
Astroがv2.7.0であればastro buildは通るが、執筆時最新のv2.10.1ではエラーでビルドできない
最新バージョンで再びビルド可能に
Webpackプラグインの作成
LoaderとPluginのデータ共有が課題。
シングルトンパターンでなんとかなるっぽい?
Webサイト
Next.js (app ディレクトリ) + molcss のサンプルを兼ねて作成。
現時点では最低限のドキュメント内容のみ。
書いた