ナビゲーション

スタイルの作成

パッケージの作成

共通の CSS ソースを公開し、各アプリで公開トークンを上書きして、本番用の利用アプリでパックしたパッケージを確認します。

このページの内容

#概要

CSS ソースを公開して、安定したスタイルの語彙を共有します。各利用アプリは自身の Master CSS エントリーにソースをインポートし、必要なトークン、ユーティリティ、ネイティブコンポーネントをコンパイルします。一つのアプリで語彙が動いてからここへ進んでください。その段階はスタイルの再利用で説明しています。

この形式のパッケージでは、アプリの生成スタイルシートやマニフェストではなくソース定義を公開します。アプリは自身のクラス利用、トークンの上書き、配信モードを選べます。ランタイムインテグレーションも提供するパッケージには、別の成果物の契約があります。

定義を共有し、アプリごとにコンパイルする

両方のアプリが同じ CSS ソースをインポートします。ネイティブコンポーネントは既定で含まれ、各アプリは自身のマークアップが要求するユーティリティを生成します。

テキストによる説明
  1. ソースを共有する. パッケージはネイティブな .btn コンポーネントと必要時生成の content-auto ユーティリティを定義します。
  2. アプリ A. マークアップは btn を使います。ネイティブコンポーネントは含まれ、未使用の content-auto は出力されません。
  3. アプリ B. マークアップは btn と content-auto を使います。出力にネイティブコンポーネントと要求したユーティリティが含まれます。

#パッケージを作成する

例では @acme/ui-theme を使います。公開前に自分のスコープへ置き換えてください。利用側はパッケージ名をインポートし、エクスポートする CSS ファイルは master.css です。

  • my-master-theme
    • package.json
    • master.css
    • README.md

パッケージルートを master.css に向けます:

package.json
{  "name": "@acme/ui-theme",  "version": "0.1.0",  "files": ["master.css"],  "style": "./master.css",  "exports": {    ".": {      "style": "./master.css",      "default": "./master.css"    }  },  "sideEffects": ["./master.css"]}

この形式にはバンドラー、TypeScript ビルド、マニフェスト生成、コンパイル済み CSS 出力は不要です。パッケージはソース CSS を公開し、Master CSS 対応プロジェクトが自身のスタイルシートグラフの一部としてコンパイルできます。


#master.css を書く

利用プロジェクトに供給する共通の語彙を master.css に置きます。

master.css
@theme {  --color-brand: #4f46e5;  --color-text-action: var(--color-text-strong);  --color-focus-action: var(--color-text-strong);  --spacing-action-x: 1rem;  --radius-action: 0.375rem;}@custom-media --theme-motion (prefers-reduced-motion: no-preference);@utility content-auto {  content-visibility: auto;  contain-intrinsic-size: auto 32rem;}@layer components {  .btn {    display: inline-flex;    align-items: center;    justify-content: center;    gap: var(--spacing-xs);    min-height: 2.375rem;    max-width: 100%;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-action);    font-size: var(--font-size-sm);    font-weight: 400;    line-height: 1.2;    background-color: var(--color-surface-raised);    color: var(--color-text-action);    box-shadow: var(--shadow-sm);    &:hover {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-focus-action);      outline-offset: 3px;    }  }}

例はカラーマーカー、ネイティブな .btn コンポーネント、必要時に生成する content-auto ユーティリティ、名前付きの theme-motion 条件を供給します。共通状態はコンポーネントが持ち、トランジションの利用はアプリが選びます。ネイティブなコンポーネント CSS は既定で含まれ、ユーティリティは使用時だけルールを生成します。

利用側に必要な最小限の公開語彙を選びます:

  • 色、間隔、半径、タイポグラフィ、影、モーション、コンテナーのトークンなどの共通値には @theme を使います。
  • メディア名には @custom-media、ほかの再利用ラッパーには @contents を持つ @mixin を使います。
  • 必要時に生成するクラスには @utility、ネイティブ CSS 内の再利用には @mixin を使います。
  • btn、field、card、toolbar などの意味を持つ UI 語彙を、パッケージが意図的に担当する場合だけネイティブの @layer components を使います。

トークンの命名はテーマトークン、構文は CSS ディレクティブ、抽象の選択はグローバルスタイルを参照してください。


#パッケージを使う

インストール後、既定の Master CSS エントリーの後にインポートします:

app.css
@import '@master/css';@import '@acme/ui-theme';

インポートしたソースは、プロジェクトの CSS エントリーグラフの一部になります。マークアップで共通の語彙をユーティリティやネイティブ CSS クラスとして使えます:

共通パッケージの語彙

<article class="p-lg b-solid b(1) b-line-divider r-lg content-auto">  <p class="flex items-center gap-xs m(0) text-xs fg-text-muted"><span aria-hidden="true" class="h(8) w(8) pill bg-brand"></span>Package brand</p>  <h2 class="mb-xs mt-sm text-lg font-regular">Shared vocabulary</h2>  <p class="mt(0) mb-md text-sm fg-text-muted">The app supplies the markup; the package supplies the shared action.</p>  <button type="button" class="btn (transition-property:box-shadow,background-color)@theme-motion (transition-duration:var(--duration-fast))@theme-motion (transition-timing-function:var(--easing-smooth))@theme-motion">Preview action</button></article>
生成された CSS
@layer components {  .btn {    justify-content: center;    align-items: center;    gap: var(--spacing-xs);    max-width: 100%;    min-height: 2.375rem;    padding: .5rem var(--spacing-action-x);    border: 1px solid var(--color-line-divider);    border-radius: var(--radius-action);    font-size: var(--font-size-sm);    background-color: var(--color-surface-raised);    color: var(--color-text-action);    box-shadow: var(--shadow-sm);    font-weight: 400;    line-height: 1.2;    display: inline-flex;    &:hover {      box-shadow: var(--shadow-md);    }    &:active {      background-color: var(--color-surface-inset);      box-shadow: none;    }    &:focus-visible {      outline: 2px solid var(--color-focus-action);      outline-offset: 3px;    }  }}@layer theme {  :root,  :host {    --color-zinc-5: oklch(96.5% .0011 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100));    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-weight-regular: 400;    --font-size-xs: .75rem;    --font-size-sm: .875rem;    --font-size-lg: 1.125rem;    --text-xs: var(--font-size-xs);    --text-xs--line-height: 1.5;    --text-xs--letter-spacing: 0;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-lg: var(--font-size-lg);    --text-lg--line-height: 1.5;    --text-lg--letter-spacing: 0;    --radius-lg: .5rem;    --spacing-xs: .5rem;    --spacing-sm: .75rem;    --spacing-md: 1rem;    --spacing-lg: 1.5rem;    --duration-fast: .15s;    --easing-smooth: cubic-bezier(.4, 0, .2, 1)  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24));    --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26))  }  :root,  :host {    --color-brand: #4f46e5;    --color-text-action: var(--color-text-strong);    --color-focus-action: var(--color-text-strong);    --spacing-action-x: 1rem;    --radius-action: .375rem  }}@layer utilities {  .text-lg {    font-size: var(--text-lg);    line-height: var(--text-lg--line-height, normal);    letter-spacing: var(--text-lg--letter-spacing, normal)  }  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .text-xs {    font-size: var(--text-xs);    line-height: var(--text-xs--line-height, normal);    letter-spacing: var(--text-xs--letter-spacing, normal)  }  .content-auto {    content-visibility: auto;    contain-intrinsic-size: auto 32rem  }  .items-center {    align-items: center  }  .bg-brand {    background-color: var(--color-brand)  }  .b-line-divider {    border-color: var(--color-line-divider)  }  .r-lg {    border-radius: var(--radius-lg)  }  .pill {    border-radius: calc(infinity * 1px)  }  .b-solid {    border-style: solid  }  .b\(1\) {    border-width: calc(1 * 1px)  }  .fg-text-muted {    color: var(--color-text-muted)  }  .flex {    display: flex  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .gap-xs {    gap: var(--spacing-xs)  }  .h\(8\) {    height: calc(8 * 1px)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mb-md {    margin-bottom: var(--spacing-md)  }  .mb-xs {    margin-bottom: var(--spacing-xs)  }  .mt-sm {    margin-top: var(--spacing-sm)  }  .mt\(0\) {    margin-top: calc(0 * 1px)  }  .p-lg {    padding: var(--spacing-lg)  }  .w\(8\) {    width: calc(8 * 1px)  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-duration\:var\(--duration-fast\)\)\@theme-motion {      transition-duration: var(--duration-fast)    }  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-property\:box-shadow\,background-color\)\@theme-motion {      transition-property: box-shadow, background-color    }  }  @media (prefers-reduced-motion: no-preference) {    .\(transition-timing-function\:var\(--easing-smooth\)\)\@theme-motion {      transition-timing-function: var(--easing-smooth)    }  }}
ボタンのホバー、押下、フォーカスでパッケージの共通状態を確認してください。保存処理は行いません。アプリはパッケージの theme-motion 条件で、動きを抑える設定に従います。

プロジェクトのエントリーがインポートしたパッケージは、すでにローカルスタイルシートから使えます。一つのスタイルシートのコンパイル時コンテキストだけに加えるには、@reference を使います:

components/Button.module.css
@reference '@acme/ui-theme';.button {  padding-inline: var(--spacing-action-x);  border-radius: var(--radius-action);  background-color: var(--color-brand);}

ローカルの .button はパッケージの間隔、半径、ブランドのトークンを使います。@reference はこのスタイルシートのコンパイル中に定義を供給し、出力には必要なテーマリソースが含まれます。ネイティブな .btn ルールや生成ユーティリティクラスはコピーしません。それらが必要ならパッケージの CSS を別に読み込んでください。ローカルスタイルシートの使い方はルートレベルのスタイルを参照します。


#利用側で公開値を上書きする

アプリ固有の選択はパッケージのインポート後に置きます。たとえばこのアプリは、コンポーネント CSS をコピーせず共通のアクションを広げます:

app.css
@import '@master/css';@import '@acme/ui-theme';@theme {  --spacing-action-x: 1.5rem;}

.btn は引き続き var(--spacing-action-x) を読みます。アプリは横パディングを 1rem から 1.5rem に変え、ほかのアプリは各自の値を保ちます。利用側が依存できるトークン名、コンポーネントセレクター、ユーティリティ名を文書化してください。JavaScript がなくても公開名の変更は、利用側のマークアップやスタイルシートに影響します。

値を上書きし、コンポーネントを保つ

利用側の上書きはパッケージのインポート後に置きます。ガイドはルート 16px で 1rem と 1.5rem を比べ、.btn のルールは共通のままです。

テキストによる説明

パッケージとアプリが一つの .btn 定義を共有する。アプリは --spacing-action-x を変え、ボタンの役割を保ったまま横パディングを広げる。

#アプリの方針をアプリに置く

再利用する記述用パッケージが、一つのアプリのソース走査、フィルター、配信方法を決めるべきではありません。

以下は利用アプリに置きます:

  • アプリ固有の @source と @source not のルール。
  • アプリ固有の @safelist と @blocklist の方針。
  • ルート、ページ、コンポーネントの一回限りのクラス。
  • 生成マニフェスト JSON とコンパイルした生成 CSS 出力。

パッケージには共通の語彙を、アプリの CSS エントリーにはローカルなソース境界、配信の選択、アプリ固有の方針を置きます。


#例で検証する

パッケージディレクトリーからソースをパックします。npm pack はローカルのアーカイブを作り、レジストリーには公開しません:

npm pack

例の名前とバージョンでは、アーカイブは acme-ui-theme-0.1.0.tgz です。master.css と想定する package.json のエクスポートが入っているか確認してください。files リストは無関係なソースやビルド成果物を除き、npm は標準のメタデータファイルも含めます。

Master CSS を統合済みの小さな利用アプリへ、そのアーカイブをインストールします:

npm install ../my-master-theme/acme-ui-theme-0.1.0.tgznpm run build

上記のマークアップを走査対象のソースに置きます。本番出力でネイティブな .btn、要求した content-auto、必要なテーマリソースを確認してください。アプリを開いてボタンの計算済みパディングを調べ、ローカルな上書きでも繰り返します。通常状態に加え、ホバー、キーボードフォーカス、動きを抑える設定も確認します。

パッケージ解決の検証には、フレームワークインテグレーションのビルドを使います。現在の単独 CLI の検査は、アプリのエントリーを読み込んでも、サードパーティのベアパッケージインポートをスタイルシートグラフ外に残す場合があります。依存一覧にパッケージの master.css がなければ、その語彙は未検証です。本番の利用アプリは、ローカルのソースだけのプレビューでは調べられないファイルとエクスポートパスも検証します。