🐼

Panda CSSで生成される2種類のクラスについて

2024/08/19に公開

Panda CSS は Chakra UI のコミュニティによって開発された、ビルド時に静的なスタイルを生成する、いわゆる Zero-Runtime CSS-in-JS ライブラリです。

この記事では Panda CSS で React コンポーネントを実装する際に、事前に把握しておくと良い 2 種類の生成されるクラスについて解説します。

Zero-Runtime CSS-in-JS ライブラリの詳細には触れないので、他の方の記事などを読んでからこの記事を読むことを推奨します。

https://zenn.dev/poteboy/articles/e9f63b87b3cd69
https://zenn.dev/osasasasa/articles/e8bc1a5caf139f

出力されるクラスの種類

Panda CSS は前述の通りビルド時に静的なスタイルを生成します。その際出力されたクラスには 2 つの種類に分けられます。それぞれのクラスの実例と特徴を見ていきましょう。

1つのスタイルだけを持つクラス

スタイルを 1 つだけ持つクラスが、定義したスタイルに対してそれぞれ生成されます。Tailwind CSS の utility classesが生成されるイメージです。

公式ドキュメントを読んだ限り明確な名称が定義されていないですが、以降は便宜上 Atomic Class と呼びます。

定義したスタイル
<button
  className={css({
    backgroundColor: 'gainsboro',
    borderRadius: '9999px',
    fontSize: '13px',
    padding: '10px 15px'
  })}
>Button</button>

<button
  className={css({
    border: '1px solid gainsboro',
    color: 'gainsboro',
    borderRadius: '9999px',
    fontSize: '13px',
    padding: '10px 15px'
  })}
>Button</button>
生成されるクラス
@layer utilities {
  .d_flex {
    display: flex;
  }

  .h_full {
    height: var(--sizes-full);
  }

  .bdr_9999px {
    border-radius: 9999px;
  }

  .p_10px_15px {
    padding: 10px 15px;
  }

  .bd_1px_solid_gainsboro {
    border: 1px solid gainsboro;
  }

  .c_gainsboro {
    color: gainsboro;
  }

  .ai_center {
    align-items: center;
  }

  .jc_center {
    justify-content: center;
  }

  .bg-c_gainsboro {
    background-color: gainsboro;
  }

  .fs_13px {
    font-size: 13px;
  }
}
レンダリングされるHTML
<button class="bg-c_gainsboro bdr_9999px fs_13px p_10px_15px">Button</button>
<button class="bd_1px_solid_gainsboro c_gainsboro bdr_9999px fs_13px p_10px_15px">Button</button>

上記の例では css 関数を利用していますが、見た目の種類を複数持つ 1 つの要素を定義できる Atomic Recipe (cva), Atomic Recipe を複数の要素に同時に適用できる Atomic Slot Recipe (sva), Chakra UI のようにコンポーネントの Props にスタイルを渡すことのできる JSX Styles Props、いくつかのスタイルをまとめて適用できる Patterns なども同じように Atomic Class が生成されます。

重要なのはこの時、重複して定義されたスタイルはまとめられるということです。例えば上記の例の両方の button 要素で定義されている padding: 10px 15px ですが、これに対応する .p_10px_15px クラスは 1 つしか生成されていません。このように Panda CSS は Atomic Class を使い回すことにより生成される CSS ファイルのサイズを最適化します。

複数のスタイルを持つクラス

上記で触れた Atomic Recipe や Atomic Slot Recipe には、事前に設定ファイルに定義しておくことができる Config Recipe、Config Slot Recipe があります。この方法を利用することで、上記の Atomic Class ではなく通常 CSS を定義する時のような複数のスタイルを持ったクラスが生成されます。

下記の例で示されているように、設定ファイルにて事前に定義したレシピを利用することで、複数のスタイルを持ったクラスを生成できます。

こちらも Atomic Class と同様に公式ドキュメントを読んだ限り明確な名称が定義されていないですが、以降は便宜上 Composition Class と呼びます。

panda.config.tsにて事前に定義したレシピ
import { defineRecipe } from '@pandacss/dev'

export const buttonRecipe = defineRecipe({
  className: 'button',
  description: 'The styles for the Button component',
  base: {
    border: '1px solid gainsboro',
    color: 'gainsboro',
    borderRadius: '9999px',
    fontSize: '13px',
    padding: '10px 15px'
  },
  variants: {
    size: {
      sm: { fontSize: '12px' },
      lg: { fontSize: '16px' }
    },
  },
  defaultVariants: {
    size: 'lg',
  }
})

export const inputRecipe = defineRecipe({
  className: 'input',
  description: 'The styles for the input component',
  base: {
    border: '1px solid gainsboro',
    color: 'gainsboro',
    borderRadius: '9999px',
  },
  variants: {
    size: {
      sm: { fontSize: '12px' },
      lg: { fontSize: '16px' }
    },
  },
  defaultVariants: {
    size: 'lg',
  }
})

export const config = defineConfig({
  ...
  theme: {
    extend: {
      recipes: {
        buttonRecipe
      }
    }
  },
  ...
});
  
レシピを利用する
import { buttonRecipe, inputRecipe } from 'styled-system/recipes';

const App = () => {
  const button = buttonRecipe()
  const input = inputRecipe()

  return (
    <>
      <button className={button}>Button</button>
      <input type='text' className={input} />
    </>
  )
}
生成されるクラス
@layer recipes {
  @layer _base {
    .button {
      padding: 10px 15px;
      font-size: 13px;
    }

    .button,
    .input {
      border: 1px solid gainsboro;
      color: gainsboro;
      border-radius: 9999px;
    }
  }

  .button--size_lg,
  .input--size_lg {
    font-size: 16px;
  }
}

レンダリングされるhtml
<button class="button button--size_lg">Button</button>
<input class="input input--size_lg" type="text">

この方法では Atomic Class と異なる点が 2 つあります。

注意点1. 使用されていないvariantsで定義されているクラスは生成されない

上記の例のレシピの定義と生成されるクラスを見ると、レシピでは variants の size に smlg を定義していますが、生成されるクラスは .button--size_lg しかなく、sm に相当するクラスは生成されていません。

このように Config Recipe を利用すると Panda CSS は使用されていない variants のクラスをビルド時に生成しません。同じく Recipe を定義して Atomic Class を生成する cva や sva も同じように variants を定義できますが、そちらは定義されたスタイルに該当する Atomic Class はすべて生成されます。

ちなみにここでは詳細には触れませんが、利用されていないスタイルも生成したい場合には、staticCssという機能を利用します。これは公式ドキュメントには Storybook などを使って、アプリケーション内で使われているかどうかに関わらずすべての UI パターンを網羅したい場合などに利用できると記載されています。またこの機能は UI コンポーネントライブラリとして静的 CSS ファイルを配信したい場合などにも利用できます。

注意点2. Atomic Classほど最適化がされない

上記の例の生成されるクラスを見ると、bordercolorborder-radius.button クラスと .input クラスの 2 つのクラスに適用されている事がわかります。このように複数のレシピや variants で利用されているスタイルは Atomic Class とは違う形で共通化された状態で生成されます。

しかし、Atomic Class と比べて最適化には限度があるようです。例えば上記の例のレシピにもう 1 つ Config Recipe を追加してみます。

新たに追加したConfig Recipe
export const checkboxRecipe = defineRecipe({
  className: 'checkbox',
  description: 'The styles for the checkbox component',
  base: {
    border: '1px solid gainsboro',
  },
})
生成されるクラス
@layer recipes {
  @layer _base {
    .button {
      padding: 10px 15px;
      font-size: 13px;
    }

    .button,
    .input {
      border: 1px solid gainsboro;
      color: gainsboro;
      border-radius: 9999px;
    }

    .checkbox {
      border: 1px solid gainsboro;
    }
  }
  ...
}

上記の例を見て分かるように、button や input と同様に border: '1px solid gainsboro', というスタイルを持っているにも関わらず、下記のように共通化されることはなく、.checkbox クラスは .button クラスや .input クラスとは別で定義されています。

期待される共通化されたスタイル
.button,
.input,
.checkbox {
  border: 1px solid gainsboro;
}

.button,
.input {
  color: gainsboro;
  border-radius: 9999px;
}

このように Recipe の最適化には限度があることがわかります。ある程度は共通化はされますが Atomic Class ほどの最適化は見込めないようです。この理由については現在 Panda CSS の GitHub Discussion にて質問しているところです。また進展があったら追記します。

https://github.com/chakra-ui/panda/discussions/2810

それぞれの使いどころ

2 つの違いがわかったところでそれぞれの方法をどのように選択すると良いか私なりの考えを共有します。

基本的にはAtomic Classを検討する

私は基本的には Atomic Class を利用するのが良いと考えます。

一番の理由は重複したスタイルをまとめて最適化してくれるため、CSS のファイルサイズを抑えることができるからです。小・中規模のアプリケーションでは CSS のファイルサイズがボトルネックとなってパフォーマンスが劣化してしまうことはあまりないですが、アプリケーションが大規模になっていくとスタイルの数を多くなっていくためリスクが増えていきます。またある程度統一した UI を持つアプリケーションであれば、大規模になっても同じスタイルを使う事が増えてきます。そのため最適化によって得られるメリットが大きくなると予想できます。

他にも取れる選択肢が多いことも挙げられます。Atomic Class では前述したように、classNames にインラインでオブジェクト形式でスタイルを定義できる css 関数、コンポーネントの定義とは別にスタイルを定義できる cva 関数や sva 関数、コンポーネントの Props としてスタイルを定義できる JSX Style Props など用途やコーディングルールによって様々な選択肢があります。しかし、Composition Class は Recipe と Slot Recipe でしか利用できないため、選択肢は狭いです。

一部の用途でComposition Classを検討する

一方で Composition Class を利用する事が良いと考えるケースもあります。

それは生成されるクラス名を完結にしたい時です。Composition Class ではクラス名が {レシピ名}--{variant名}_{variantの値} のように BEM 形式に近い形で生成されます。そのため、クラス名を見ることで用途や状態がわかりやすくなっています。

例えば複数のアプリケーションで共通利用する UI コンポーネントライブラリを Panda CSS で実装する時を考えます。デザインや既存の設計の問題で汎用的に作られた UI コンポーネントをそのまま利用できず、アプリケーション側でクラス名を参照してスタイルを上書きしたいという事はよくあります。そういった際に Atomic Class を利用していると、クラス名が汎用的なものになっているため可読性が悪いです。また特定のルールに従ったクラスを設定したとしても、ブラウザ上で特定がしづらいため、利用の難易度が上がってしまいます。そんな時に Composition Class を利用すると、クラス名の可読性が上がり、スタイルの上書きもしやすいです。

まとめ

Panda CSS が生成するクラスの 2 つの種類について紹介しました。
もちろん2つの方法は併用できますが、コンポーネントによって書き方を変えてしまうとコードの可読性や統一性を損ねてしまいますので、あまり推奨しません。

Panda CSS はとても開発体験がよく便利ですが、多くのスタイルの手法がありますので、それぞれの特徴や生成されるクラスの種類を理解しておくことが重要です。この記事で触れた 2 種類のクラスやそれぞれを生成する API を把握した上で、用途やチームの方針にあった方法を選択しましょう。

社内で共通利用される UI コンポーネントライブラリの実装において、 Panda CSS の利用を検討する際に参考にしていただける記事を書いていますので、よろしければそちらも併せてご覧ください。

https://zenn.dev/moneyforward/articles/2ba21d684965b9

関連リンク

https://panda-css.com/
https://zenn.dev/poteboy/articles/e9f63b87b3cd69
https://zenn.dev/osasasasa/articles/e8bc1a5caf139f
https://zenn.dev/cybozu_frontend/articles/panda-is-coming
https://zenn.dev/cybozu_frontend/articles/panda-output

GitHubで編集を提案
Money Forward Developers

Discussion