⚙️

型安全でatomicなZero-Runtime CSS in JS

に公開

今回紹介するのは Plumeria という新しい CSS-in-JS ライブラリです。
Zero-Runtime で動作し、Atomic CSS と型安全な API が特徴的です。
official documentation

Rustベースライブラリのコンパイラを備えており、確定値ベースのコンパイルが可能で、ゼロランタイムの弱点であるAST解析による変数展開をクライアントでは必要としません、サーバーサイドではプラグインを通して変数のインライン展開をします。

※この記事は2025/3月13日に公開したものを2025/7/8日に書き直したものです。

執筆時点でReact Native for WebやStyleXの思想と非常に似ており、createとglobalのAPIによってオールインワンでCSSを綺麗に剥がせる点が気に入っています。

他の興味あったCSS-in-JS

vanilla-extractStyleXLinariaKuma-UIPanda CSS
基本的に今はZero-Runtime時代であり、アトミックが使えないCSS ModulesやランタイムJSのEmotioinやStyled JSXは古いという認識です。

これらの色々試しましたがスタイルを当てる方法とオブジェクトにしっくり来て今回紹介に至りました。

Plumeriaの主な機能

メインのAPIはcss.createです、これだけでほぼ完結でき、atomicなクラスを生成してくれます。

import { css } from '@plumeria/core'

export const styles = css.create({
  text: {
    color: 'skyblue', // xxxhash1
  },
  box: {
    width: 40, // xxxhash2
    height: 40, // xxxhash3
  },
})

const classNames = css.props(styles.text, styles.box);
// classNames = "xxxhash1 xxxhash2 xxxhash3"

API一覧

  1. css.create: 型安全かつatomicクラスを生成するオブジェクトを作ります。
  2. css.props: createの返り値のオブジェクトをハッシュ化されたクラス名に変換します。
  3. css.defineConsts: 変数ではないリテラルの静的定数値を定義します。
  4. css.defineVars: CSS変数を定義します。
  5. css.defineTheme: テーマを定義します。
  6. css.keyframes: ハッシュを返り値とするアニメーションを作成します。
  7. css.global: グローバルスタイルを定義できます。
  8. rx: React限定の機能で動的変数(主にuseState用)をcss変数に繋げます。

css.props()スタイルオブジェクトからクラス名を生成する関数でcss.createと一緒に使用します。

<div className={css.props(stylex.box, state ? styles.color1 : styles.color2)}>

インストール

ViteとNext.js、Webpackに対応しています。
今回はWebpackは省略します。

pnpm i @plumeria/core

コンパイラー

pnpm i -D @plumeria/compiler

Next.js

pnpm i -D @plumeria/next-plugin
import type { NextConfig } from "next";
import { withPlumeria } from "@plumeria/next-plugin";

const nextConfig: NextConfig = withPlumeria({
  /* config options here */
});

export default nextConfig;

Vite

pnpm i -D @plumeria/vite-plugin
import { defineConfig } from 'vite';
import plumeria from '@plumeria/vite';
 
export default defineConfig({
  plugins: [..., 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',
    },
  },
})

コマンドライン

npx css

通常のコンパイルで速度が一番速いです。

引数

--type-check
TypeScriptのタイプチェッカーを走らせます。タイミングはコンパイルを行う直前です。
これをビルドパイプラインに組み込むことで型安全性を未然に検知できます。
--view
通常のコンパイルに加えてコンパイルしたcssのログ見ることが出来ます。
--paths
通常のコンパイル+ファイルの在りどころを相対パスで表示します。

ESLint Plugin

これだけだと型チェックがCSSの値に効かなくて、誤字ったときに間違ってコンパイルされてしまい開発体験が中途半端です、一応エラーレベルは手動で調整できるのでプロジェクトに応じて設定できます。

@plumeria/eslint-plugin

  • no-destructure
    メインAPIの分割代入(destructuring)にエラー警告を出します。
    これはcss.createとcss.globalが文脈上cssからのチェーンの方がコンパイル速度が速いためです。

  • no-inner-call
    関数内の記述にエラーを警告します。
    これは関数内だとコンパイルできない訳ではなくコンポーネントのレンダリングに混ざるためです。

  • sort-properties
    recess式のソート(stylelint-config-recess-orderからリストをフォークで頂いています)

  • validate-values
    値の検証を行いエラーや警告を出します。1ヶ月ほど掛けて作成しました。
    非推奨と実験機能を除くプロパティが全てリストされてあります。

  • no-unused-keys
    関数の中にある呼び出しされていないキーに警告を出します。
    これで使ってないキーを消すことが簡単になります。

エコシステム

zss-engineというZero-Runtime CSS-in-JS特化のライブラリです。 Plumeriaもこれで作られているみたいです。
名前はZero-runtime Style Sheet Engineの略みたいです。
少し触ったところtranspilerやinjectCSSなどの誰でも現代のCSS-in-JSが作成できる関数が入っています。

使ってみた所感

ESLintで型チェックが効くのが良いところです。ソートも機能するためstylelintでソートしているようにセーブ時に並び替えられるので使っていてストレスを感じません。

atomicアーキテクチャでプロパティ単位での冗長化をなくしているためCSSバンドルサイズが抑えられます。
CSS Modulesよりパフォーマンスが出るのと、開発者がTypeScriptに慣れてるのであれば素のCSSより圧倒的に堅牢です、これはCSSを手動で構築するよりも効率的なソリューションです。

感想としてPlumeriaはReactに全振りしたStyleXvanilla-extractpigment-cssの中間にいると思います。
間違いなく色々使ってきた中で大規模アプリケーション向けだと言えるでしょう。
Vue.jsやSolid, Preact, Svelte、React-Router(Remix)などでも動きました。

CSS詳細度

CSS詳細度はcss.propsの引数の順番で完全に決まります。
プロパティの衝突が起こった際には右にあるスタイルのプロパティが常に優先されます。
ショートハンド vs ロングハンドのルールについても通常のCSSと同じ挙動をします。

classNameの展開をオブジェクトで行う

例えばclassNameはスニペットを展開する時に文字列展開をしますがオブジェクト展開するようにVSCodeを設定できます、これはオブジェクト系のCSS-in-JSを使う上で非常に便利な設定になります。

Command + Shift + P
スニペット: スニペットの構成
Snippets: Configure Snippets

TypeScriptを選ぶ

{
  "React className": {
    "prefix": "c",
    "body": "className={$1}",
    "description": "React className with curly braces",
  }
}

最後に

CSSは絶対これしかダメ!!
みたいなのって全くなくて、色んなライブラリを触って試したりするのがいいと思います。
特に1個に固執しないで洋服や靴を変えるようにその日の気分で変えたりするものだと思います。
ただその中で推しのライブラリが出てくるとなんかリッチでいいですよね♪

Discussion