型駆動型 styling framework・Plumeria
今回紹介するのは Plumeria という新しい CSS frameworkです。
※この記事は2025/3月13日に公開したものを2026/02/01日に書き直したものです。
型のみで動作し、Atomic CSSを吐き出します。
Plumeria official documentation
Linariaやpigment-css、StyleXの思想とからヒントを得ており、本体は型宣言のみで値を持ちません、宣言自体はhashに置き換わり完全に消滅します。
使用する側では引数のオブジェクトでスタイルを書くだけですが、それ自体が型定義そのものです。
JSを書いているようで、値を持たないので実は型(スキーマ)を定義しているだけになります。
Zero-Runtime の弱点と createStatic
swcベースのコンパイラを備えており、スタティックベースのAST抽出に特化しています。ゼロランタイムの弱点であるAST解析による変数展開はcreateStaticを通して行われます。
他の興味あったCSS-in-JS
vanilla-extract・StyleX・Linaria・Kuma-UI・Panda CSS
現在は Zero-Runtime と Atomic CSS が主流になりつつあり、
ランタイム依存の CSS-in-JS や非 Atomic な手法とは思想が大きく異なります。
これら色々試しましたが、スタイルを当てる方法として今回はオブジェクトにしっくり来ました。
Plumeriaの主な機能
主要APIはcss.createとcss.propsをメインで使います、これだけでほぼ完結できます。
import { css } from '@plumeria/core';
const styles = css.create({
text: {
fontSize: 12,
color: 'navy',
},
size: {
width: 120,
},
});
const className = css.props(styles.text, styles.size);
コンパイル後:
className = "xhrr6ses xvbwmxqp xhk51flp"
生成される CSS:
.xhrr6ses:not(#\#) {
font-size: 12px;
}
.xvbwmxqp {
color: navy;
}
.xhk51flp {
width: 120px;
}
API一覧
-
css.create: 型安全かつatomicクラスを生成するオブジェクトを作ります。 -
css.props: createの返り値のオブジェクトをハッシュ化されたクラス名に変換します。 -
css.keyframes: アニメーションネームを返り値とするアニメーションを作成します。 -
css.viewTransition: トランジションネームを返り値とするビュートランジションを作成します。 -
css.variants: バリエーション引数を受け取れる関数を返します。 -
css.createTheme: テーマやデフォルトで変数値を定義します。 -
css.createStatic: 変数ではないリテラルの静的定数値を定義します。
css.props()スタイルオブジェクトからクラス名を生成する関数でcss.createと一緒に使用します。
<div className={css.props(stylex.box, state ? styles.color1 : styles.color2)}>
Install Core
pnpm i -D @plumeria/core
Next.js
next-pluginは宣言を消して最適化する+devモード時のHMRを担当します。
postcss-pluginは静的抽出用です。
pnpm i -D @plumeria/next-plugin @plumeria/postcss-plugin
import type { NextConfig } from "next";
import { withPlumeria } from "@plumeria/next-plugin/turbopack";
const nextConfig: NextConfig = withPlumeria({
/* config options here */
});
export default nextConfig;
postcss.config.jsをプロジェクトのルートに配置
module.exports = {
plugins: {
'@plumeria/postcss-plugin': {
include: '**/*.{ts,tsx}',
exclude: ['**/node_modules/**', '**/.next/**'],
},
},
};
@plumeriaマークを付けたcssをエントリーポイントのapp/layout.tsxでimportします。
マークしたCSSファイルを対象にPlumeriaで記述したアトミックCSSをビルド時に抽出し統合します。
/* app/global.css */
@plumeria;
メディアクエリー
使い慣れたメディアクエリの構文をセレクタとして使用できます。
import { css } from '@plumeria/core'
const styles = css.create({
header: {
'@media (max-width: 768px)': {
position: 'absolute',
top: 0,
},
},
})
擬似要素(クラス)
styled-componentsの様に使い慣れた擬似要素-クラスの構文でセレクタとして使用できます。
import { css } from '@plumeria/core'
const styles = css.create({
text: {
color: '#333'
':hover': {
color: '#515151',
textDecoration: 'underline',
},
},
})
ESLint Plugin
型チェックを強化できます。
CSSの値にESLintが効くようになりタイプミスしたときに間違ってコンパイルされることを未然に防げます。
@plumeria/eslint-plugin
エコシステム
ほとんどが@swc/coreとzss-engineというライブラリで作られているみたいです。
swcは言うまでもなくAST解析やトランスフォーム中心のbabel置き換えを出来るライブラリで
す。
後者はcss-in-jsにフォーカスされたライブラリで、名前はZero-runtime Style Sheet Engineから略が来てるみたいです。
使ってみた所感
ESLintで型チェックが効くのが良いところです。ソートも機能するためstylelintでソートしているようにセーブ時に並び替えられるので使っていてストレスを感じません。
atomicアーキテクチャでプロパティ単位での冗長化をなくしているためCSSバンドルサイズが抑えられていました。
CSS Modulesよりパフォーマンスが出るのと、開発者がTypeScriptに慣れてるのであれば素のCSSより圧倒的に堅牢です、これはCSSを手動で構築するよりも効率的なソリューションです。
感想としてPlumeriaはゼロランタイムに極振りしたlinariaとStyleXの中間に位置すると思います。
間違いなく色々使ってきた中で、安定を目的とした大規模アプリケーション向けでした。
Solid, Preact, React-Routerなどでも動きました。
※:(vueとsvelteではtsファイルとして使う必要があり)
バンドルサイズ
これがCSS in JSにおいてかなり重要です、ここで15 kBとかあると初期コストに+15 kBかかるため使うのに躊躇しますが、Plumeriaの配信サイズはBundle.jsで調べると以下のようになっていました:
@plumeria/core@7.0.2:
Bundle size is 326 B -> 202 B (brotli)
つまり配信時にバンドラーによってbrotli化されるため202Byteしかありません。
v6.0.0でprops関数もスタブ化されたため、ハッシュマップすら残しません。
ゼロオーバーヘッドでVanilla CSSと同じかつ、アトムでそれ以上のCSSバンドル効率を叩き出します。
メンテナンス性
モノレポのパッケージは全てOIDC化されておりCIもtestにインテグレーションされて公開ワークフローとして最適化されています。
型定義にcsstypeを使用しているから1.25mbじゃんか!と思うかもしれませんが、実際には型定義はランタイムで動かないためバンドルサイズにも含まれません。
そのMDN自動生成されてる型のメリットとして将来的に採用されるCSSプロパティの型付けもバージョンによって追従して行われていることです。
なのでcsstypeのバージョンを自動で上げるだけで近代のCSSに自然に対応してる形となります。
実現されていて驚いたこと
swc(Speed Web Compiler)というRustのクレートで実装されているJS版のswc/coreの機能を静的解析にフルに使われています。
これによりビルド時間短縮の最大化が行われており、正に過度な最適化の結果という感じでした。
注釈※: このライブラリにはbabelは一切使われていません
結局誰に向いているか
次のような場合にヒットすると思います。
- 大規模アプリケーションになる想定のプロジェクト
- 管理しやすいを前提にした設計を組んでる方
- CSS をロジックとして扱いたい方
- TypeScript を主戦場にしているチームなど
CSS詳細度
CSS詳細度はcss.propsの引数の順番で完全に決まります。
プロパティの衝突が起こった際には右にあるスタイルのプロパティが常に優先されます。
ショートハンド vs ロングハンドのルールについても通常のCSSと同じ挙動をします。
classNameの展開をオブジェクトで行う
例えばclassNameはスニペットを展開する時に文字列展開をしますがオブジェクト展開するようにVSCodeを設定できます、これはオブジェクト系のCSS-in-JSを使う上で非常に便利な設定になります。
Command + Shift + P
スニペット: スニペットの構成
Snippets: Configure Snippets
TypeScriptを選ぶ
{
"React className": {
"prefix": "css",
"body": "className={css.props($1)}",
"description": "React className with css.props()",
}
}
Discussion