🐻

EmotionユーザーがKuma UIを試してみたら結構いい感じ

2023/07/16に公開

スタイリングのライブラリまじで何使えばいいの問題

ご存知の通り、React界隈のスタイリングライブラリは良くも悪くも選択肢が多いです。正解をもとめてTwitterの奥地を彷徨ってみたけど、やっぱりわからない。

https://twitter.com/yuneco/status/1648681299132153856

元々スタイリング周りって、単にピュアなCSSを書きやすくしてくれる(本当の意味での)ユーティリティから、デザインシステムやUIコンポーネントまでを内包したフレームワークまで多種多様なグラデーションがあるので、雑に議論すると発散したり喧嘩になったりしやすい領域です(私は選択肢があることは素敵だと思うけど、それ自体も異論はあると思う)。

特にここ最近は

  • ①Tailwindとその発展系として、デザインシステム自体を安全にコードに組み込んでいきたい
  • ②RSCの実用化(というかNext)とそもそものパフォーマンス懸念からスタイリングをできるだけ静的にしたい(ゼロランタイム)

あたりのモチベーションで雨後の筍のごとくいろんなライブラリが出てきている状態ですね。たぶん。
いわゆるゼロランタイムを謳う系統だと、

あたりでしょうか。どれも結構すごいなー、って思うし完成度も決して低くはないと思います。その反面採用するには怖い部分もあって、

  • ビルドの仕組みに深く依存するので、ビルド環境やフレームワークのバージョンが上がると動かなくなることがある
    • 最近だと「LinariaがViteのバージョン上げたら動かなくなった」とか。大抵問題の根が深いので、アプリを作りたいだけの開発者にはそう簡単には直せない
    • つまり、つらい
  • 原理上、動的なスタイルの適用やJavaScriptからCSSへの定数の注入が難しい
    • 各ライブラリでいろいろ工夫してくれているけど、頑張るほどに独自記法になっていく
    • つまり、つらい

ってあたりを考えてZennとQiitaとSOとTwitterを3周くらいした挙句

  • まあやっぱりEmotionか...
  • まあやっぱりCSS Modulesか...

のどちらかに帰着することが多いです。

Kuma UIはEmotion好きにとっての次の選択肢になるか?

そんな中新たに出てきたのが🐻‍❄️Kuma UI🐻です。
Kumaは多分領域的にはPandaのライバルになるんじゃないかな、と思うのですが、それとは別の軸としてEmotionユーザーの取り込みも狙っているようです。今日はv1リリースされたばかりのKumaをEmotionユーザーの視点で触ってみたいと思います。

Kuma全体の概要は作者さんが既に記事に書かれているので、↓こちらを参照くださいませ。

https://zenn.dev/poteboy/articles/d94573793d56ed

すごくざっくり書くと

  • Utility Propsを使ってタグにざくざくスタイルを書ける(<Box p={2} />みたいな感じ)
  • styled-component・emotionでお馴染みのstyled("div")`...` css`...` も使える
  • 基本的にビルド時に静的なCSSになる(ゼロランタイム)
  • 動的なスタイルを書いたらそこだけランタイムで処理される(ハイブリッド)

Emotion使いにとって重要なのは後ろ3つです。つまり、Emotionと同様に生CSSに近いスタイルをTypeScriptで安全に書きつつ、静的な部分はオーバーヘッドのない形にビルド時に処理してくれる理想の仕組みに見えます。

ぶっちゃけEmotion使ってても本当に動的なスタイリング(propの値でスタイルが都度変わるようなもの)ってほとんどやらないので、「じゃあ🐻‍❄️の方がよくない?」っていうのが今回のお試しの動機になります(前置き長いな)。

その上で、🐻‍❄️ができる子って安心できたら徐々にUtility Propsを使ったスタイリングも導入していけたら...夢が広がりますね。

とりあえず入れてみる

今回はViteで作ったReact + TypeScriptのプロジェクトにKuma入れて試します。

まず、Kuma本体とVite用のモジュールインストールして

npm i @kuma-ui/core @kuma-ui/vite

vite.config.tsをちょっと修正

+ import KumaUI from '@kuma-ui/vite';
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
-  plugins: [react()],
+  plugins: [react(), KumaUI()],
})

あとは書くだけ

import { css } from "@kuma-ui/core";

const style = css`
  color: salmon;
`;

function App() {
  return <div className={style}>Hello Kuma!</div>;
}

export default App;

深く掘ればもっと色々あるのかもだけど、とりあえず動かすだけならこれだけ。
あれ...Emotionより導入楽なんじゃ🤔

どこまでEmotionと同じ使い方ができるか試す

準備ができたので、実際にどこまで使えるか試していきます。
大前提として、Kumaは静的にビルドされてこそなので、Emotionと同レベルの動的スタイリングは求めません

じゃあどこまでできるのかな?という部分ですが、期待するもののレベルとして、以下の順にみていきます。

  1. 固定のスタイルがcssで良い感じに書ける
  2. 擬似クラス・擬似要素・メディアクエリが良い感じに書ける
  3. 定数をTypeScript側から差し込める
  4. 変数をTypeScript側から差し込める(動的スタイリング)

固定のスタイルがcssで良い感じに書けるか?

とりあえず雑に何か書いてみます。

const buttonStyle = css`
  padding: 0.25em 1em;
  height: 2em;
  border-radius: 1em;
  font-weight: bold;
  background: linear-gradient(to bottom, salmon, powderblue);
  border: none;
  color: white;
  cursor: pointer;
`;

function App() {
  const [count, setCount] = useState(10);
  const countUp = () => setCount((c) => c + 1);
  return (
    <button className={buttonStyle} onClick={countUp}>
      Kuma: {count}
    </button>
  );
}

export default App;

Kumaのcss関数で基本的な静的スタイル

ばっちりですね🐻

擬似クラス・擬似要素・メディアクエリが良い感じに書けるか?

もうちょっといろいろやってみましょうか🐱

const buttonStyle = css`
  padding: 0.25em 1em;
  height: 2em;
  border-radius: 1em;
  font-weight: bold;
  background: linear-gradient(to bottom, salmon, powderblue);
  border: none;
  color: white;
  transition: all 0.2s;
  cursor: pointer;
  &:hover {
    box-shadow: 0 0 0.4em #0003;
    scale: 1.05;
  }
  &:active {
    transition-duration: 0.05s;
    box-shadow: 0 0 0.2em #0003;
    scale: 0.95;
    filter: brightness(0.9) contrast(1.2);
  }
  &::before {
    content: "🐻";
    display: inline-block;
    padding-right: 0.5em;
  }
  @media screen and (max-width: 360px) {
    &::before {
      content: "🐱";
    }
  }
`;

Kumaのcss関数で擬似要素・擬似クラス・メディアクエリ

完璧ですね🐻

定数をTypeScript側から差し込めるか?

次は定数の差し込みです。↓みたいなやつです。

const SIZE = 24;
const style = css`
  font-size: ${SIZE}px;
`;

私が個人的にCSS-in-JSを使いたいのは(型安全の理由以外では)このあたりが大きなポイントです。

やってみましょう。

const UPPER_COLOR = "salmon";
const LOWER_COLOR = "powderblue";

const buttonStyle = css`
  padding: 0.25em 1em;
  height: 2em;
  border-radius: 1em;
  font-weight: bold;
  background: linear-gradient(to bottom, ${UPPER_COLOR}, ${LOWER_COLOR});
  border: none;
  ...
`;

Kumaのcss関数でTypeScript側の定数を使う

おっと...ダメでした😇

ドキュメントを見ると注意書きがありますね

Please note that the css API does not currently support interpolations in the same way as libraries like Emotion does. However, we are actively working to incorporate this feature, so stay tuned for updates.

「今のところEmotionと同じように変数を埋め込むことはできないけどできるように頑張ってるよ」って感じでしょうか。

人間の目にはこの${UPPER_COLOR}, ${LOWER_COLOR}salmon, powderblueに静的に置換できることは自明ですが、ビルド時の静的解析でそれをやるのは簡単ではないのでしょう。JavaScriptに本当の意味での定数があれば良いんですけどね。

Utility Propsならできる?

先ほどの結果にはもうひとつ残念な点があります。仮に静的なスタイルとしての評価ができなかったとしても、ハイブリッドなアプローチで動的なスタイルとして動いてほしい、という点です。

どうやら現状、ハイブリッドに動作するのはKuma独自のUtility Propsのみらしいので、先ほどの例をUtility Props版に書き換えてみます。

const UPPER_COLOR = "salmon";
const LOWER_COLOR = "powderblue";

function App() {
  const [count, setCount] = useState(1);
  const countUp = () => setCount((c) => c + 1);

  return (
    <Button
      p="0.25em 1em"
      bg={`linear-gradient(to bottom, ${UPPER_COLOR}, ${LOWER_COLOR})`}
      color="white"
      borderRadius="1em"
      border="none"
      fontWeight="bold"
      height="2em"
      cursor="pointer"
      transition="all 0.2s"
      _hover={{
        boxShadow: "0 0 0.4em #0003",
        transform: "scale(1.05)",
      }}
      _active={{
        transitionDuration: "0.05s",
        boxShadow: "0 0 0.2em #0003",
        transform: "scale(0.95)",
        filter: "brightness(0.9) contrast(1.2)",
      }}
      _before={{
        content: ['"🐱"', '"🐻"'],
        display: "inline-block",
        paddingRight: "0.5em",
      }}
      onClick={countUp}
    >
      Kuma: {count}
    </Button>    
  );
}

Utility Propsなら定数も埋められる(ただし動的スタイル扱い)

こんな感じで${UPPER_COLOR}, ${LOWER_COLOR}の部分に定数を差し込めることが確認できました。ほぼ機械的に書き換えただけなので、Kumaのスタイリングの機能はおそらくあまり活かせていませんが、逆に言えば書き換え自体は平易にできそうです。

クラス名が🦄になっているのはKumaがこのスタイルを静的なものだと認識できなかったことを意味します。作者さんの紹介記事で「<Box fontSize={10 + 6} />みたいな書き方は静的と解釈できないよ」って説明があったので、${UPPER_COLOR}なら行けるかな...と思ったのですがダメみたいです。この辺りは今後に期待ですね。

あとちょっと残念な点としては、Utility Propsの記法では現状いくつかサポートされていないCSSプロパティがあるようです。上の例だとfiltercontentは赤線が引かれてしまって機能しませんでした。scale, rotate, translateの個別変形プロパティもダメだったのでtransformで書く必要があります。

とはいえこの辺りは本質的な問題ではないので、随時追加してもらえるのでは、と思います。

変数をTypeScript側から差し込めるか?(動的スタイリング)

最後は本当の意味での動的なスタイリングですね。個人的にはここまではそんなに求めないのですが、せっかくなのでみてみましょう。
cssを使ったEmotion的な書き方は既に定数の時点で脱落してしまったので、Utility Propsで確認します。

const UPPER_COLOR = "salmon";
const LOWER_COLOR = "powderblue";

function App() {
  const [count, setCount] = useState(1);
  const countUp = () => setCount((c) => c + 1);

  return (
    <Button
      fontSize={(count % 3)*10 + 10}
      bg={count % 2 ? UPPER_COLOR : LOWER_COLOR}
      onClick={countUp}
    >
      Kuma: {count}
    </Button>    
  );
}

クリックするごとに文字サイズと背景色を切り替えています。

KumaのUtility Propsで動的スタイリング

ばっちり動いていますね🐻
🦄で動的に付与されるクラス名も6クリックごとにループしています。

スタイルの再利用はされる?

クラス名がループしているのでスタイル自体をキャッシュして再利用しているのかな…とも思ったのですがそういうわけではないようです。

たしかEmotionはキャッシュ使って保持とかやっていたような気もするのですが、Kumaは使い捨てのようです。とはいえこの辺りは私にはどっちが効率的なのかもわからないので、詳しい方がいらっしゃったら教えてください。

いずれにしてもEmotionだろうとKumaだろうと、そんなに頻繁にスタイルの書き換えを行うなら普通のインラインスタイルにすべきでしょうから、大きな問題ではないのかもしれません。

まとめと個人的な希望

というわけでまとめ

Kumaのいいところ

  • 導入がびっくりするくらい超楽。なんだこれ
  • 生CSSに近い書き方から、Utility Props&テーマの独自定義まで、選択の自由度が高い
  • Emotionやstyled-componentのユーザーができるだけ障壁なく移行できることを考えてくれている

(個人的に)ここまではできてくれたら嬉しいのだけど…

  • TypeScriptからの定数の埋め込み

定数ができれば変数もまあ大差ない話だと思うので、これ自体が高い望みなのは理解しつつ…でも色とかサイズとかイージングとか、TS側で定数管理したいことは多々あるので、やっぱりできるだけピュアなTypeScript&CSSに近い方法でサポートしてくれたら嬉しいなぁ…って思います。

現状で使えるか?

Emotionの代替、というのは厳しい気もするけど「CSS ModulesをTS側に書けるもの」くらいに割り切って使えば体験はすごく良いです。Kumaのcss関数でできることはEmotionでもできるので、Kumaでダメだったら後からEmotionに戻すのも比較的簡単。なので気軽に試してみて良いと思います。

Utility Propsは好き嫌いはあると思うけど、これも書き味はとてもいいです。ただ、今回試した範囲でもちょっと不足に見える部分はあったし、大前提としてライブラリの記法にロックされる要素は少なからずあるので、様子を見ながら試していくと吉かな、って感触です。

軽く触ってみただけなので、全体通して間違いなどあれば優しくコメントくださいませ🐈

Discussion