Skip to content

ガイド ​

CSS-Zero を使うと、コントラクトファイル(.css.ts/.css.js)で CSS スタイルを記述できます。ビルド時にコンパイラがこれらのファイルを実行し、各ユーティリティ呼び出しを決定的な文字列トークンに置き換えて、対応する CSS を単一のチャンクとして出力します。ブラウザでは何も実行されません。

CSS-Zero は vanilla-extract(型安全な .css.ts 記述、ゼロランタイム)と Tailwind CSS(合成可能なユーティリティ/バリアントクラス)に触発されており、単一のアイデアに統合されています:すべては再利用可能なトークンです。

なぜ「再利用可能なCSS in TS」と呼ばれるのですか? ​

私の考えでは、コードの価値は多くの場合、その再利用性によって測ることができます。CSS-Zero は、複雑さや厳しい制約なしにスタイルを再利用可能にすることを中心に構築されています。

唯一の要件は、.css.ts/.css.js という拡張子のファイルにスタイルを書くことです。そこから先は、再利用がすべてのレベルで第一級の概念になります:

  • 再利用可能なルール — className、classSelector、id、idSelector、variable、animation、font、layer、container は、自由に合成できる小さく自己完結したトークンを生成します。
  • 再利用可能なバリアント — variants と theme は、単一の基本スタイルを一連のモディファイアに変えるため、1 つのコンポーネントで CSS を重複させることなく多くの視覚状態をカバーできます。
  • 再利用可能な条件付きスタイル — style(config, deps) を使うと、参照されたトークンがバンドル内で実際に使用されている場合にのみ出力されるスタイルを追加できます。
  • 再利用可能なライブラリ — .css.ts モジュールは単なるモジュールです。スタイルライブラリを NPM に公開し、任意のプロジェクトで通常の依存関係として、他のエクスポートと同様にトークンをインポートして利用できます。ライブラリは .css.js に事前ビルドすることもできます — CSS-Zero は両方の形式を理解するため、利用者は配布方法を気にする必要はありません。

各ユーティリティはプレーンな文字列、または配列/フラットなオブジェクト内の文字列を返します — それだけです。ビルド時、コンパイラは各一意の結果をトークンと呼ばれる特別な文字列(例:o-s_1、--o-v_1)に置き換え、対応する CSS を出力します。トークンは一意であるため、未使用のスタイルは tree-shaking で除去されます — 実際に使用する CSS だけが出荷され、この再利用はランタイムで何もコストがかかりません。

結果:スタイルを一度定義すれば、どこでも再利用でき、使用するものだけを出荷できます。

インストール ​

bash
npm i @css-zero/core @css-zero/vite-plugin

セットアップ(Vite) ​

ts
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { cssZero } from '@css-zero/vite-plugin';

export default defineConfig({
    plugins: [react(), cssZero()],
});

.css.ts ファイルにスタイルを書き、コンポーネント内でトークンをインポートします。

tsx
// Button.tsx
import { btn } from './styles.css.ts';

export function Button() {
    return <button className={btn}>Click</button>;
}

ユーティリティ ​

個別ルール ​

ユーティリティシグネチャ戻り値
className(rule?) => string一意のクラストークン(例:o-s_1)
classSelector(rule?) => [string, string][token, '.token']
id(rule?) => string一意の id トークン
idSelector(rule?) => [string, string][token, '#token']
variable(config?) => [string, string][--token, 'var(--token)']
animation(config?) => string一意の @keyframes 名トークン
font(config?) => string一意の @font-face ファミリートークン
layer() => string一意の @layer token 文字列
container(type?) => [string, string][container, '@container name']

合成ルール ​

ユーティリティシグネチャ戻り値
variants(config, base?) => Record<string, string>モディファイアトークンのマップ、.base.mod セレクタ
theme(vars, options) => [Record, Record][varRefs, optionClasses]
style(config, deps?) => stringグローバルスタイル(deps なし)または条件付き AND チャンク

例 ​

className ​

ts
import { className } from '@css-zero/core';

export const btn = className({
    color: 'white',
    '&:hover': { color: 'gray' },
});
// → 'o-s_1'

classSelector ​

ts
import { classSelector } from '@css-zero/core';

export const [card, cardSelector] = classSelector({
    display: 'block',
    padding: '16px',
    borderRadius: '8px',
});
// card → 'o-s_1', cardSelector → '.o-s_1'

id ​

ts
import { id } from '@css-zero/core';

export const badge = id({ backgroundColor: 'red' });
// → 'o-i_1'

idSelector ​

ts
import { idSelector } from '@css-zero/core';

export const [modal, modalSelector] = idSelector({
    position: 'fixed',
    inset: 0,
});
// modal → 'o-i_1', modalSelector → '#o-i_1'

variable ​

ts
import { variable } from '@css-zero/core';

// [name, var(name)]
export const [accentKey, accent] = variable('#2b6cb0');
// → ['--o-v_1', 'var(--o-v_1)']

animation ​

ts
import { animation } from '@css-zero/core';

export const spin = animation({
    from: { transform: 'rotate(0)' },
    to: { transform: 'rotate(360deg)' },
});

font ​

ts
import { font } from '@css-zero/core';

export const inter = font({
    src: "url('/fonts/inter.woff2') format('woff2')",
    weight: '400 700',
    display: 'swap',
});
// → 'o-f_1' — use it as `fontFamily: Inter`

layer ​

ts
import { layer } from '@css-zero/core';

export const base = layer();
// → '@layer o-l_1'

container ​

ts
import { container } from '@css-zero/core';

export const [card, cardQuery] = container('inline-size');
// card → 'o-c_1 / inline-size', cardQuery → '@container o-c_1'

variants ​

ts
import { variants } from '@css-zero/core';

export const tone = variants(
    { primary: { color: '#fff' }, ghost: { color: 'gray' } },
    btn // optional base token → selector becomes `.btn.primary`
);
// → { primary: 'o-s_2', ghost: 'o-s_3' }

theme ​

ts
import { theme } from '@css-zero/core';

// [varRefs, optionClasses]
export const [tokens, options] = theme(
    { accent: '#2b6cb0', spacing: '8px' },
    { dark: { accent: '#0f0' }, compact: { spacing: '4px' } }
);
// tokens.accent → 'var(--o-v_1)'
// options.dark  → 'o-s_1' (overrides accent when applied)

style ​

ts
import { style } from '@css-zero/core';

// no deps - always emitted
style({ body: { margin: 0 } });

// emitted only if all deps are used in the bundle
style({ [`${btnSelector} > span`]: { display: 'block' } }, [btnSelector]);

型の拡張 ​

インターフェース宣言のマージを使って、独自のカスタム CSS プロパティを統合します:

ts
declare global {
    namespace CSSZero {
        interface ExtraProperties {
            '--brand': string;
        }
    }
}

スタイルライブラリの公開 ​

コントラクトモジュールは通常のモジュールなので、スタイルを NPM パッケージとして公開して利用できます:

ts
// your-lib/src/buttons.css.ts
export const [btn, btnSelector] = classSelector({ /* ... */ });
export const btnBg = variants({ primary: { /* ... */ }, ghost: { /* ... */ } }, btn);
tsx
import { btn, btnBg } from 'your-lib/src/buttons.css';

<button className={`${btn} ${btnBg.primary}`}>Button</button>

プラグインオプション ​

オプション型デフォルト説明
prefixstring'o'各トークンの名前プレフィックス(o-s_1、--o-v_1)。
build.inlinebooleanfalse<link> の代わりに <head> 内の <style> タグに CSS をインライン化する

重要 ​

バンドラーの外(SSR、Jest、tsc/IDE)では、@css-zero/core は空文字列を返します — シグネチャは正しく、値はプレースホルダーです。実際の動作経路は @css-zero/compiler と @css-zero/vite-plugin を通ります。

パッケージ ​

パッケージ役割
@css-zero/core実際のユーティリティ型 + tsc/IDE 用の文字列スタブ
@css-zero/compilerビルドツールに依存しないコンパイルエンジン
@css-zero/vite-pluginVite ファサード:コンパイル、集約、CSS のインライン化/出力、インポートの除去

Apache License 2.0に基づいて公開されています。