mcpField
P
panda-css-theming

panda-css-theming

github.com/chakra-uiデザイン・UI6.2k71 回インストール今日更新
リポジトリ

panda-css-theming について

Panda CSSのテーマとデザインシステム設計を扱うスキルです。トークンの階層、テーマ、複合スタイル、レシピの役割や命名を整理し、既存の設定や使用箇所を基に、局所スタイルを共有APIへ昇格させる判断を支援します。

主な機能

  1. 01プリミティブとセマンティックトークンの設計
  2. 02テーマとカラーモードの設計
  3. 03複合スタイル、レシピ、スロットレシピの区別
  4. 04トークンとバリアントの命名規則
  5. 05解決済みデザインシステム仕様の確認
  6. 06スタイルをレシピにする時期の判断

活用シーン

  1. 01Panda CSSプロジェクトのトークン階層を構築
  2. 02テーマとカラーモードの扱いを統一
  3. 03繰り返すコンポーネントスタイルをレシピ化
  4. 04既存デザインシステムのトークン由来を監査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/chakra-ui/panda --skill panda-css-theming

インストール後、Claude Code で/panda-css-themingを入力して読み込まれたか確認します。

詳細

Panda CSS のテーマ設定とシステム設計

これはアーキテクチャ層です。トークンを何と呼ぶか、どの層に置くか、スタイルを呼び出し箇所からレシピに昇格させるタイミングを決めます。コードベースに呼び出し箇所が数百もあると、こうした選択を覆すのは困難です。

ほとんどの場合、次の2つの質問で判断できます。

  • この値には意味がありますか。それとも単なる値ですか。 意味がある値は semanticTokens に定義します。
  • このファイルの外で、この値を変える必要がありますか。 必要ならレシピにします。必要ないなら css() を使います。

用語

以下の用語をそのまま使ってください。Panda の設定キーはこれらの用語に基づいているため、ここで曖昧な言葉を使うと、後で誤った設定キーにつながります。各項目では、避けるべき言葉とその理由を説明します。

プリミティブトークン:theme.tokens に定義する生のスケール値で、値そのものにちなんだ名前を付けます(blue.500、spacing.4)。避けるべき言葉:「変数」(Panda はトークンから CSS 変数を生成しますが、両者は同じものではありません)、「ベーストークン」(条件を表す { base: … } と受け取られるため)。

セマンティックトークン:theme.semanticTokens に定義する値で、果たす役割にちなんだ名前を付けます(fg.muted、danger)。条件ごとに値を変えられます。避けるべき言葉:「エイリアス」(エイリアスは固定の参照先を意味しますが、セマンティックトークンは条件ごとに異なる値を持てます)、「テーマ変数」。

コンポジットスタイル:textStyles、layerStyles、または animationStyles に定義する、名前付きのプロパティのまとまりです。避けるべき言葉:「プリセット」(Panda ではプリセットは共有可能な設定パッケージを指し、まったく別のものです)、「ミックスイン」。

レシピ:コンポーネントのスタイルとそのバリアントで、ファイル内の cva または設定内の theme.recipes から定義します。避けるべき言葉:「バリアント関数」、「スタイルオブジェクト」(スタイルオブジェクトは css() に渡す通常の引数です)。

スロット:スロットレシピの名前付きパーツの1つで、個別にスタイルを設定します。避けるべき言葉:「パート」、「要素」、「サブコンポーネント」。設定キーは slots なので、それ以外の言葉を使うと一貫性が失われます。

アナトミー:コンポーネントが公開するスロットの完全な一覧です。これは公開 API です。避けるべき言葉:「構造」、「形状」。

バリアント軸:variants 内の1つのキー(size、visual)で、その軸の選択肢を保持します。避けるべき言葉:「プロパティ」(プロパティは利用者が渡すものですが、軸は定義するものです)、「修飾子」。

複合バリアント:特定の軸の組み合わせに一致したときに適用されるルールです。避けるべき言葉:「コンボバリアント」、「マルチバリアント」。

パターン:再利用可能なレイアウト関数です(stack、grid、container)。パターンをコンポーネントと呼ぶこと、レシピをパターンと呼ぶことは避けてください。Panda では、それぞれ意味も設定キーも異なる用語として使われています。

条件:状態またはメディアセレクターのキーです(_hover、_dark、md)。避けるべき言葉:「修飾子」、「擬似クラス」(条件にはメディアクエリーやセレクターも含まれます)。

ステップ1:既存のものを確認する

  1. panda.config.ts:presets、theme、semanticTokens、conditions、strictTokens、designSystem。
  2. <outdir>/types/tokens.d.ts:現在解決できるすべてのトークン名。
  3. <outdir>/specs/design-system.json:同じトークンとその値、条件、レシピ、各トークンの由来となるプリセット。詳しくはデザインシステム仕様を参照してください。
  4. <outdir>/recipes/:すでにレシピになっているもの。
  5. トークン化しようとしているプロパティについて、呼び出し箇所をいくつか grep します。8つのファイルで副次テキストに gray.600 を使っているなら、それが不足しているセマンティックトークンです。

何もないところから設計しないでください。プリセット(@pandacss/preset-base、@pandacss/preset-panda)には、すでにスケールが含まれています。置き換えるのではなく、拡張してください。

トークンのレイヤー

4 つのレイヤーには、それぞれ異なる役割があります。値は、その意味を正直に表せる最も上位のレイヤーに配置してください。

1. プリミティブトークンは theme.tokens に定義します。使い道についての意見を含まない、生のスケール値です。

tokens: {
  colors: { blue: { 500: { value: '#3b82f6' } } },
  spacing: { 4: { value: '1rem' } },
}

ルール:今日必要な 3 つの色合いだけではなく、スケール全体を定義します。用途ではなく、値が 何であるか に基づいて名前を付けます(blue.500 など)。 プリミティブの名前にコンポーネントを含めてはいけません。

2. セマンティックトークンは theme.semanticTokens に定義します。値が 何を意味するか、また条件ごとにどう変化するかを表します。

semanticTokens: {
  colors: {
    fg: { value: { base: '{colors.gray.900}', _dark: '{colors.gray.50}' } },
    'fg.muted': { value: { base: '{colors.gray.600}', _dark: '{colors.gray.400}' } },
    danger: { value: '{colors.red.600}' },
  },
}

ルール:

  • ライト/ダークの切り替えを扱うのに適した場所はここだけです。両方の値を 1 つのトークンにまとめてください。ここで一度条件分岐できたものを、呼び出し側で _dark によって分岐させてはいけません。
  • 役割に基づいて名前を付けます。fg、fg.muted、bg.subtle、border、danger などです。gray600Text や buttonBlue のような名前は避けてください。
  • 1 つのコンポーネントだけが使うトークンは、セマンティックトークンではなくレシピの値です。2 つ目の利用箇所が現れたときに昇格させてください。それより前に昇格させる必要はありません。
  • プリミティブは {colors.gray.900} で参照してください。セマンティックトークンに 16 進数を直接書いてはいけません。

3. 複合スタイルは textStyles、layerStyles、animationStyles です。常に一緒に適用されるプロパティをまとめて名前を付けます。

textStyles: {
  heading: {
    h1: { value: { fontSize: '2.5rem', fontWeight: 'bold', lineHeight: '1.2' } },
  },
}

同じ 3~5 個のプロパティが常に一緒に現れ、チーム内で口に出して呼ばれている名前がある場合に使います。バリアントが必要なら、レシピにしてください。

4. レシピは、バリアントを持つコンポーネントスタイルです。以下を参照してください。

命名
  • 見た目より役割を優先します。danger はブランド変更後も使えますが、red は使えません。
  • 階層にはドットを使います。fg、fg.muted、fg.subtle のようにします。Panda はこれらを 1 つの名前空間にフラット化するため、プレフィックスがグループを表します。
  • 軸を 1 つ選び、一貫して使います。同じテーマで bg.subtle と subtleBackground を混在させると、どちらか一方に統一するよりもコストがかかります。
  • キーワードとの衝突に注意してください。lineHeights のトークン名に normal を使うと、CSS のキーワードを上書きします。トークン名を変更してください。

テーマとカラーモード

セマンティックトークンには両方の値を保持します。どちらを適用するかを決める仕組みは、別途必要です。

カラーモード

ベースプリセットでは、_dark は「dark クラスを持つ要素の内側」を意味します。クラスを切り替えると、すべてのセマンティックトークンが反転し、コンポーネントはその変化を意識する必要がありません。

<html class="dark"></html>

データ属性にも対応させるには、条件を再定義してください。設定の変更なので、その後にコード生成を実行してください。

conditions: {
  extend: {
    dark: '.dark &, [data-theme="dark"] &',
  },
}

useColorModeValue のようなフックを使ったり、コンポーネント内で条件分岐したりしないでください。条件がそのための仕組みです。

複数のテーマ

themes には、1 つの属性で切り替えられる代替トークン値を定義します。デフォルトと異なる値だけをオーバーライドし、オーバーライドする各値には両方のモードを指定してください。

themes: {
  gothic: {
    semanticTokens: {
      colors: {
        canvas: { value: { base: '{colors.gray.100}', _dark: '{colors.black}' } },
        accent: { value: { base: '{colors.purple.700}', _dark: '{colors.purple.300}' } },
      },
      radii: { control: { value: '{radii.none}' } },
    },
  },
}
<html data-panda-theme="gothic"></html>

テーマとモードは別々の軸です。data-panda-theme がテーマを選び、dark クラスがそのテーマ内のモードを選びます。両者は組み合わせて適用されます。テーマは属性を持つ要素と、その内側のすべての要素に適用されるため、<html> に 1 つのテーマを設定し、その内側のパネルに別のテーマを設定できます。

テーマの配信

テーマの配信方法を指定するまで、Panda は themes の内容を出力しません。選択肢は 2 つあり、それぞれ独立しています。

メインのスタイルシートに含める。 staticCss.themes にテーマを列挙します。各テーマは、オーバーライドする値だけを含む変数ブロックとして tokens レイヤーに出力されます。ランタイムは不要で、リクエストは 1 回ですが、すべてのページで全テーマ分のデータが読み込まれます。

staticCss: {
  themes: ['matcha', 'gothic'],
}

必要に応じて読み込む。 <outdir>/themes に各テーマが個別の JSON ファイルとして保存され、要求されたときに取得されます。テーマが多数ある場合や、実行時にユーザーがテーマを選ぶ場合に適しています。

import { getTheme, injectTheme } from '../styled-system/themes'

const theme = await getTheme('gothic')
injectTheme(document.documentElement, theme)

injectTheme は、要素に data-panda-theme を設定し、そのテーマの変数を含む <style> を追加します。

選び方
  • 1 つの外観で、ライトとダークを用意する場合:_dark を使ったセマンティックトークンを使用します。themes ブロックは不要です。
  • 固定ブランドが少数の場合:themes と staticCss.themes を使用します。
  • テーマが多数ある場合、または実行時にユーザーが選択する場合:themes と getTheme/injectTheme を使用します。

トークンセットの大部分をオーバーライドするテーマがある場合、セマンティックレイヤーが薄すぎる可能性があります。テーマでオーバーライドするのは役割であり、スケールを再構築することではありません。

デザインシステムの仕様

この仕様の出力はオプトインです。panda codegen、panda build、または panda lib に --spec を渡すと、 <outdir>/specs/design-system.json に書き出されます。--spec=<file> を指定すると、別の場所に出力されます。 フラグを指定しなければファイルは書き出されず、panda dev ではこのフラグ自体を指定できません。

サーバーも MCP も介さず、解決済みのシステム全体を1つのファイルにまとめます。

  • schemaVersion と、各トークンカテゴリーを paths の対応する部分に割り当てる categories
  • paths と tokens。すべてのトークンのカテゴリーと cssVar が含まれます
  • values。各トークンと解決済みの値を対応付けます(animations.spin は spin 1s linear infinite です)
  • conditions。各名前の実際のセレクターが含まれます(_hover は &:is(:hover, [data-hover]) です)
  • sources。各トークンの由来となったプリセットまたはファイルを示します。tokens[path].source はこの項目を参照します
  • themes と、プロジェクトで定義されている場合は recipes、slotRecipes、patterns、textStyles、layerStyles、 animationStyles、keyframes、colorPalettes。空のセクションは省略されます。

トークンの値や CSS 変数、またはどのプリセットで定義されたかを確認するときに読みます。名前が存在するかどうかだけを 手早く確認するなら、<outdir>/types/tokens.d.ts がユニオン型で、grep も高速です。

このファイルがない場合、たいていは codegen の失敗ではなく、誰も --spec を指定しなかったことが原因です。 panda codegen --spec を実行してください。代わりに panda.config.ts を読まないでください。このファイルにあるのは 誰かが記述した内容であり、プリセットをマージした後に Panda が解決した内容ではありません。また、 @pandacss/preset-base と @pandacss/preset-panda が使われていると、トークンセットの大部分はそこに現れません。

ファイルはあるものの古い場合は、以前の実行で書き出されたものです。自動更新されることはないので、 もう一度 panda codegen --spec を実行してください。

テーマの監査はここから始めます。source がプリセットのトークンは継承されたものなので、プロジェクト独自の追加分は ローカルソースのものです。この違いに、ずれが生じていることがよくあります。

DESIGN.md を作成する

チームがエージェント向けの人間可読な概要を必要とする場合は、手作業ではなく、このファイルから作成してください。仕様には トークン名、値、条件、レシピのバリアント、由来など、すべての事実が含まれています。一方、何を意図しているかまでは 示せません。たとえば、fg ではなく fg.muted を使う場面、使用してはいけないスケール、ブランドが避けていることなどです。

意図を記述し、残りは生成して、どの仕様の実行結果を元にしたかをファイルに記載してください。トークンの値を文章に 手作業で転記しないでください。誰かがテーマを編集すると古くなりますが、再生成は panda codegen --spec を 1回実行するだけです。

これを定期的に行うプロジェクトでは、誰かがフラグを覚えていることに頼らず、ビルドスクリプトに --spec を指定してください。

スタイルをレシピにするタイミング

はしごです。一段ずつ上がり、テストに合格した場合にだけ進みます。

呼び出し元の css({ … })。 すべてはここから始まります。スタイルが一度しか使われない間は、ここにとどめます。

↓ 同じスタイルオブジェクトが3か所以上に現れる

コンポーネントファイル内の cva。 バリアントを持つローカルレシピで、唯一の利用箇所と同じ場所に配置します。

import { cva } from '../styled-system/css'

const button = cva({
  base: { px: '4', py: '2', rounded: 'md' },
  variants: {
    visual: { solid: { bg: 'accent' }, outline: { borderWidth: '1px' } },
    size: { sm: { fontSize: 'sm' }, md: { fontSize: 'md' } },
  },
  defaultVariants: { visual: 'solid', size: 'md' },
})

↓ このファイルの外部の利用箇所でもバリアントが必要になる、またはスタイルがコンポーネントではなくデザインシステムに属する

設定レシピ。 panda.config.ts の theme.recipes の下で defineRecipe を使います。これでバリアント API がシステムの一部となり、 型が生成され、CSS は設定から出力されます。

↓ コンポーネントが複数の要素をレンダリングし、それぞれに独自のスタイルが必要になる

スロットレシピ。 theme.slotRecipes の下で defineSlotRecipe を使うか、ファイル内で sva を使います。

必要のない段まで上がらないでください。1つのファイルで使うスタイルに設定レシピを使うと、間接性が増し、型付けのコストもかかります。

スロットレシピを使う場合

コンポーネントが、利用側で個別にスタイルを適用したり置き換えたりできるパーツ(ルート、トリガー、コンテンツ、タイトル、インジケーターなど)をレンダリングする場合は、スロットを使います。目安は次のとおりです。

  • 内側の要素にスタイルを適用するため、className を渡している。
  • 子コンポーネントのマークアップに踏み込むネストセレクターを書いている。
  • 2つの要素が同じバリアントに異なる反応をする必要がある。
const card = sva({
  slots: ['root', 'header', 'body'],
  base: {
    root: { rounded: 'lg', borderWidth: '1px' },
    header: { px: '4', py: '3', borderBottomWidth: '1px' },
    body: { px: '4', py: '4' },
  },
  variants: {
    density: {
      compact: { header: { py: '2' }, body: { py: '2' } },
    },
  },
})

まず構造を設計します。スタイルを書く前にスロットに名前を付けてください。スロットの一覧は公開 API なので、後から名前を変更するとすべての利用側に影響します。

レシピをスロットレシピに変換する方法: コンポーネントがレンダリングする要素を列挙し、それぞれに名前を付け、各スタイルブロックを対応するスロットの下に移動します。次に、各バリアントで対象となるスロットを指定するよう、バリアントを移動します。base とすべてのバリアントを、フラットなスタイルオブジェクトからスロットをキーとするマップに変更します。その後、利用側を createRecipeContext から createSlotRecipeContext に切り替えます。

JSX でのレシピの使用(v2)
  • createRecipeContext(recipe) は、cva レシピ用の { withContext } を返します。
  • createSlotRecipeContext(recipe) は、sva 用の { withRootProvider, withProvider, withContext } を返します。 withRootProvider は、独自のスロットをレンダリングしないルート向けです。

どちらも <outdir>/jsx から利用できます。v1 の単一の createStyleContext は廃止されました。

バリアント API の設計

  • 実装ではなく意図に基づいてバリアントに名前を付けます。 visual、size、tone などです。isBlue は避けてください。
  • 必ず defaultVariants を設定します。 設定しないと、何も渡さない利用側にはベースのスタイルのみが適用されます。実行時プロパティとして渡されたバリアントには、いずれにしてもベースとデフォルトのみが適用されます。
  • ブール値のバリアントは true/false のキーを使います。本当に二者択一となるものに限定してください。
  • 複合バリアントは CSS を増やします。 組み合わせごとに出力されます。4つのビジュアル、4つのサイズ、3つのトーンの組み合わせでは、潜在的に48個のブロックが生成されます。v2 では複合バリアントがビルド時に即時出力されます。optimize.smartCompoundVariants: true を設定すると、実際に使われる組み合わせに出力を絞れます。
  • 複合バリアントを増やすより、バリアントを増やします。 マトリクスのほとんどのセルに複合ルールが必要なら、軸の設定が適切ではありません。
  • レスポンシブなバリアント値を使えます(size={{ base: 'sm', md: 'md' }})。ただし、抽出可能なリテラルに限ります。

isolatedDeclarations のもとでエクスポートするコンポーネントでは、バリアントのキーを注釈して CSS が .d.ts に出力されないようにします。

import type { RecipeRuntimeFn } from '../styled-system/types'

export const button: RecipeRuntimeFn<{ visual?: 'solid' | 'outline' }> = cva({ … })

SlotRecipeRuntimeFn と StyledComponent<Tag, Props> も、それぞれ sva と styled で同じ役割を果たします。

アンチパターン

  • プリミティブをひとまとめにしただけのテーマ。 呼び出し側がすべて gray.600 を参照していると、ブランド変更の際にアプリ全体で検索と置換が必要になります。それを防ぐのがセマンティックレイヤーです。
  • グローバルトークンに含まれるコンポーネント名。 colors.buttonPrimary のようにすると、将来の各コンポーネントが独自の名前を作ることになります。利用側ではなく、役割に名前を付けてください。
  • コンポーネントごとのスケールの再定義。 base でスペーシング値を再定義するレシピは、テーマから乖離しています。
  • レシピ内のページレイアウト。 レシピはコンポーネント向けです。ページの構成には css() とパターンを使います。
  • 呼び出し側でのダークモードの分岐。 セマンティックトークン内の _dark ひとつで、コンポーネント内に50個の指定を設けずに済みます。
  • 念のためのスロット追加。 スタイルを適用しないスロットも、維持しなければならない API になります。

設定の変更後

panda codegen(または panda build)を実行します。トークン、レシピ、およびそれらの型は、実行するまで生成されません。その後、CSS の差分を確認します。トークン名の変更によって値が気付かないうちに失われた場合、出力されなくなった CSS で確認できます。

正しく動作している状態

  • リブランドの変更は設定に反映され、呼び出し箇所には影響しません。
  • _dark が現れるのは theme.semanticTokens で、ほかにはほとんどありません。
  • dark クラスまたは data-panda-theme を切り替えるとページが変化し、どのコンポーネントもそのどちらも読み取りません。
  • themes のすべてのテーマが staticCss.themes に列挙されているか、getTheme 経由で読み込まれているため、何も出力されないテーマはありません。
  • コンポーネント全体から gray.600 のようなプリミティブを検索しても、ほとんど見つかりません。
  • レシピの slots 配列にあるすべてのスロットに、そのスタイルが定義されています。
  • トークン名にコンポーネント名が含まれていません。
  • panda codegen がエラーなく実行され、レシピ用に生成された CSS が、リリースするつもりだったバリアントと一致しています。

関連項目

  • 設計したシステムに沿ってスタイルを記述するには、Skill ツールで「panda-css」を呼び出してください。
  • createStyleContext の分割を含む v1 から v2 への移行については、Skill ツールで「panda-css-migrate」を呼び出してください。

コメント