🐻‍❄️

ゼロランタイムUIコンポーネントライブラリ「Kuma UI」の紹介

2023/07/14に公開1

先日、ゼロランタイムで動くUIコンポーネント & CSS-in-JS ライブラリ Kuma UI のメジャーバージョンをリリースしました🎉

https://twitter.com/poteboy/status/1678237698371485696s

このライブラリの開発を始めたいきさつは👇の記事で述べている通りで、Chakra UIのような書き味をゼロランタイムで実現することは可能なのか技術的な検証として始めたのがきっかけでした。
https://zenn.dev/poteboy/articles/e9f63b87b3cd69

開発を続けていく中で徐々に形になり、数多くの助けと支えがあって無事にメジャーバージョンをリリースすることができました。応援して下さった皆様には感謝してもしきれません。

さて、本記事ではそんなKuma UIの特徴や使い方、今後の展望について解説させて頂きたいと思います🐻‍❄️

Kuma UI とは

Kuma UIはゼロランタイムで動くUIコンポーネント & CSS-in-JS ライブラリです。

Chakra UIMUIのような既存のUIコンポーネントライブラリとの主な違いとしては、Kumaはユーザーがコンポーネントに適用したスタイルをビルドプロセス中に抽出し、CSSに変換します。
これにより、ランタイムでのスタイリング処理を避けることができ、パフォーマンスの大幅な向上が期待できます。また、Next.jsのApp Routerといった最新のテクノロジーと組み合わせても問題なく動作します。

先日Next.jsの公式ドキュメンテーションにおいてもApp Router対応済みのCSS-in-JSライブラリとしてKumaが言及されました(ここではクライアントコンポーネントに対応と記されていますが、実際にはサーバーコンポーネントでも動きます)。
https://nextjs.org/docs/app/building-your-application/styling/css-in-js

書き味としてはChakra UIとほぼ同様で、ライブラリが提供するUtility Propsを使用して各コンポーネント群に以下のようにインラインでスタイリングを行います。その他にも、styledcssと言ったお馴染みのAPIも用意していて、Emotionのような構文でも書くことができます。

import { Box, Heading, css } from "@kuma-ui/core"

function App() {
  return (
    <Box as="main" display="flex" flexDir={["column", "row"]}>
      <Heading className={css`font-sze: 24px;`} >
        Kuma UI
      </Heading>
    </Box>
  );
}

また、Kumaはヘッドレスコンポーネントを採用していて、デフォルトでコンポーネントに色がついていません。この理由として、筆者がChakraを使っていた際にコンポーネントに自動でpaddinggapがつく点があまり好きではなく、機能だけを提供してほしいと思っていたからです(ただし、この点に関してはデフォルトでThemeを提供して欲しいとの要望も頂いていて、インポートして使えるTheme群も今後開発予定です)。

そして何と言ってもKuma UIの最大の特徴は、Hybrid CSS-in-JS という手法を採用している点です。この手法を採用しているライブラリは古今東西探しても現時点では存在せずKumaが世界初となっています。

次章では、Hybrid CSS-in-JSとは一体何で、どんな課題を解決するのかについて詳しく説明します。

Hybrid CSS-in-JS とは何か

https://www.kuma-ui.com/docs/Concepts/Hybrid

Hybrid CSS-in-JS は一言でいうとゼロランタイムCSS-in-JSとランタイムCSS-in-JSの良いとこ取りを実現した手法です。静的に解析可能なスタイルはビルド時に抽出し、動的に変化する値のみJavaScriptのランタイムで処理することで、書き方を制限することなくパフォーマンスを最適化します。

この手法が何故嬉しいかを説明するために、まずはランタイムCSS-in-JSとゼロランタイムCSS-in-JSそれぞれのメリットとデメリットを紹介します。

ランタイムCSS-in-JS

ランタイムで動くEmotionstyled-componentsのような従来のCSS-in-JSライブラリはCSSをJavaScriptのみで表現することを可能にし、スタイリングに圧倒的な表現力をもたらしました。

const Component = styled.div< { primary: string }>`
  background: ${({ primary }) => (primary ? 'palevioletred' : 'transparent')};
  color: ${({ primary }) => (primary ? 'white' : '#333')};
`

// 以下のようにpropsを渡せる
<Component primary={true} />

これらのライブラリは、JSで書かれたCSSオブジェクトを実行時にParseして有効なCSS文字列に変換し、変換された文字列を<style />タグに直接埋め込むと言う手法を用いて実現しています。

しかし、この手法はJSオブジェクトのParse処理とDOMへの直接埋め込みという負荷の大きい処理を実行時に行う必要があり、パフォーマンス上の問題点がありました。

また、これらのライブラリがServer Componentで動作しない理由は、サーバー側でコンポーネントのレンダリング処理が行われるためにDOMアクセスができず、<style />タグへのCSS埋め込みができないためです。

上記により、Server ComponentやNext.jsのApp Router登場以降は徐々にランタイムCSS-in-JS離れが起こってしまいました。

ゼロランタイムCSS-in-JS

そこで登場したのがLinrariaVanilla ExtractのようなゼロランタイムCSS-in-JSでした。

これらのライブラリは実行時ではなくコンパイル時にCSSの抽出を行うことで、実行時のオーバーヘッドを取り除くことに成功しました。

しかし、これらのライブラリでは書き方に制約が多く、JavaScriptが持つ本来の表現力を十分に発揮することができませんでした。例えばVanilla Extractではcss.tsファイルのみにしかスタイルを記述できないという制約があります。筆者の個人的な意見を述べると、そのような制約下では外部CSSファイルに直接CSSを書くのと殆ど変わりはないと思っています。

このように、ゼロランタイムCSS-in-JSではパフォーマンスの利点と引き換えにJSの表現力が失われることとなりました。

Hybrid = Zero-Runtime + Runtime 💜

ゼロランタイムとランタイム、この2つの手法の良いとこ取りを実現したのがKuma UIのHybridアプローチです。

先述の通り、Kumaではビルド時に定まるスタイルを静的解析で抽出し、ビルド時には定まらないスタイルに関しては実行時にDirty Checkingを行うことで、JavaScriptの表現力を失うことなくパフォーマンス最適化を実現しました。

実際の挙動を確認するためにここでは以下の例を見てみましょう。

function App() {
  const [checked, toggle] = useReducer((state) => !state, false);
  return (
    <Box>
      <Text fontSize="24px" color={checked ? "red" : "blue"}>
        Hello World
      </Text>
      <Button onClick={toggle}>Click Me</Button>
    </Box>
  );
}

上記コードスニペットでは、ボタンをクリックした際にTextコンポーネントのカラーがblueからredに変化します。この例では、fontSize="24px"は変化しえないのでビルド時にCSSが生成され、colorの値は変化しうるために実行時にCSSを生成します。

Kumaでは、静的に抽出された値と実行時に注入された値のデバッグを容易にするために静的に定まったCSSに関してはクラス名に🐻というprefixをつけ、実行時に注入されたCSSに関しては🦄というprefixをつけています。

誤解を恐れずに言うと、ランタイムでスタイルを注入するのはDOM操作のオーバーヘッドがあるので避けれるのであれば避けるに越したことはありません。

Next.jsのApp RouterのようにServer Componentを提供するフレームワークでKumaを使用する場合、クラス名に1つでも🦄 prefixが含まれている場合はそのコンポーネントは自動的にClient Componentに変換されてしまいます。

よって、インラインスタイルやdata属性を活用してできる限り🦄クラスを減らす設計にすることがベストプラクティスであると言えるでしょう。

余談: Kuma UIの名前の由来🐻‍❄️

ここで1つ余談ですが、クラス名のprefixが🐻(熊)と🦄(ユニコーン)であるのには、ちゃんと理由があります。

これはKuma UIというライブラリ名の由来にも関係するのですが、元々このライブラリの名前の由来は筆者が早稲田大学出身で、早稲田大学は創設者である大”隈”重信氏の名前にちなんで熊がマスコットキャラクターとして採用されており、そちらにちなんでKumaと名付けたのがきっかけでした。

そして早稲田大学には永遠のライバル校として慶應大学の存在があります。慶應大学ではマスコットにユニコーンが採用されるケースがしばしばあり、ライブラリ内でセルフ早慶戦がしたかった、という理由で🐻と🦄を採用しました。ちなみにビルド時にはこれらのクラス名はkuma-という文字列に変換されるので、この挙動は本番環境では再現しません。

Kuma UI の使い方

さて、ここまででKuma UIの特徴について説明してきたので、本章ではKuma UIの実際の使い方について紹介します。

Kuma UIのコンポーネントはNext.js v13.4以降でStableとなったApp Routerでもサーバーコンポーネントとして問題なく動作するので、今回はその環境を用いてインストールをしてみます。

npx create-next-app@latest

続いてKuma UIの必要パッケージ群をインストールします。

npm i @kuma-ui/core @kuma-ui/next-plugin

パッケージのインストールが完了したら、next.config.jsを以下の編集します。

next.config.js
+ const { withKumaUI } = require("@kuma-ui/next-plugin");

/** @type {import('next').NextConfig} */
const nextConfig = {};

+ module.exports = withKumaUI(nextConfig);

以上でKumaのセットアップは完了です。それでは実際にコンポーネント側でKumaを使ってみましょう。

page.tsx
import { Box, Text } from "@kuma-ui/core";

export default function Home() {
  return (
    <Box as="main">
      <Text fontSize={24} fontWeight={"bold"}>
        Welcome to Kuma 🐻‍❄️
      </Text>
    </Box>
  );
}

上記のように記述すると、実際にコンポーネントに追加したスタイルが追加されていることが確認できます。

Kumaでは上記コードスニペットで使用したBoxText以外にも沢山のコンポーネントを提供しておりますので、是非公式ドキュメントよりご確認ください。

テーマ

Kumaではコンポーネント毎にVariantを設定できるTheme機能を提供しています。

Theme機能を利用するにはRootディレクトリにkuma.config.tsファイルを作成します。

kuma.config.tsファイルの中でcreateTheme関数を呼び出し、各コンポーネントにVariantを設定します。

kuma.config.ts
import { createTheme } from "@kuma-ui/core";

const theme = createTheme({
  components: {
    Button: {
      variants: {
        primary: {
          bg: "rgb(29, 155, 240)",
          p: "8px 24px",
          color: "white",
          borderRadius: "16px",
        },
      },
    },
  },
});

export default theme;

また、以下のようにして@kuma-ui/coreの型をオーバーライドすることで、実際のコンポーネント側で使用する際にVariantに型推論がつきます。

type UserTheme = typeof theme;

declare module "@kuma-ui/core" {
  export interface Theme extends UserTheme {}
}

https://www.kuma-ui.com/docs/Theme/CustomizingTheme

レスポンシブスタイル

Kumaでは画面サイズに応じて変化するスタイルを記述する際は@mediaクエリを使う代わりに以下のようにStyle Propsに配列を渡すことで実現できます。

<Flex flexDir={['column', 'row']} />

画面サイズの閾値となるBreakpointsはデフォルトで以下のように設定されています。上記Flexコンポーネントの例では画面サイズが576px未満の際はflex-directionの値がcolumnとなり、それより広くなった際はrowに変化します。

export const defaultBreakpoints = Object.freeze({
  sm: "576px",
  md: "768px",
  lg: "992px",
  xl: "1200px",
});

また、Breakpointの値は先ほど作成したkuma.config.tsにて以下のように変更可能です。各キーの名前も自由に設定可能です。

const theme = createTheme({
  breakpoints: {
    small: "500px",
    medium: "800px",
    large: "1100px",
    xlarge: "1400px",
  },
});

また、こちらで設定したBreakpointはcssstyledAPI内でも使用することが可能です。もちろん、500pxのように直接値を入れることも可能です。

export const Flex = styled("div")`
  display: flex;
  flex-direction: row;
  @media (max-width: sm) {
    flex-direction: column;
  }
`;

こちらも詳細については公式ドキュメントに記載しておりますので、そちらも併せてご確認ください。

https://www.kuma-ui.com/docs/Theme/Breakpoints

今後の展望

ここまででKuma UIの特徴と基本的な使い方について説明しました。
最後に、Kuma UIの今後の展望について現在の構想を紹介したいと思います。

静的解析精度の向上

先述の通り、Kumaではコンパイル時に静的解析できなかったpropを動的な値としてみなし実行時に処理します。すなわち、例え動的に変化しうる値ではなかったとしても静的解析ができなかった値は全てDynamic Propsとして扱われます。

例えば、以下のコンポーネントを実行した場合fontSizeの値は16になると思いますが、現在のKumaのコンパイラはこの値をビルド時に解析することができません。

<Box fontSize={10 + 6} />

計算を必要とする値を静的に確定させることはスコープやクロージャ、他ファイルからのインポートなど様々なケースをカバーする必要があり、想像以上に難しいです。しかし、少しずつ静的解析の精度を高めて、今後はより複雑なケースも対応していきたいと考えています。

styledcss のInterpolation

現在Hybrid機能を提供しているのはKuma固有のコンポーネント群のみで、styledcssAPIは依然として動的なpropsを受け取ることができません。

const Component = styled('p')`
 color: ${({ primary }) => (primary ? 'blue' : 'white')};
`

Emotionやstyled-componentsなどのライブラリからKumaに移行を考えていらっしゃる方々にとっては、この制約は無視できないものと考えておりますので、こちらについても対応を考えています。

他のフレームワークへの対応

Kumaは理論上JSXを使用しているフレームワークであれば全て対応可能です。なので、RemixやSolid、Preactなどのフレームワークに関しても随時対応を予定しております。

また、大変光栄なことに先日Wakuという新しいReactフレームワークを開発しているDaishi Katoさんの方から、KumaでWakuをサポートしないかと言うご提案をいただき、現在はこちらの対応を最優先で動いています。

https://github.com/poteboy/kuma-ui/issues/208

テーマ機能の拡充

Kumaではコンポーネント毎にテーマを設定できたり、色をカスタマイズすることが可能ですが、現時点ではそれ以上の機能はありません。

フォントやスペーシング、Shadowについてもテーマからデザイントークンを設定することができればより表現力の幅が広がると考えているので、これらのトークンについても今後拡充を予定しています。

また、先ほど申し上げた通りデフォルトでスタイルを付随しないヘッドレスなコンポーネントはKumaのメインコンセプトの1つではありますが、MVPやプロトタイプのためにサクッとプロダクトを作りたいと言ったケースの際にはデフォルトでコンポーネントにスタイルが施されていた方が良い場合もあるかと存じます。

なので、@kuma-ui/coreからインポートして使えるテーマ群の提供も今後予定しています。

おわりに

Kuma UIはメジャーバージョンをリリースしましたが、追加したい機能はまだまだ沢山あり、今後もさらに進化していきたいと思っています。

この記事を読んで少しでも良いなと思った方は、是非GitHubでスターを頂けると嬉しいです。今後も継続して開発を続けていく上で大変励みになります。

https://github.com/poteboy/kuma-ui

また、Kumaにこんな機能が欲しい、こう言う点を改善して欲しいなどのご意見も大歓迎です。公式TwitterDiscordサーバーも用意しておりますので、興味がある方は是非ご参加ください。

そして最後に、Kumaを開発するに当たってコアメンバーとして一緒に取り組んで下さったNaritomiさんとKotaroさんの協力なしではここまで到達することは絶対に不可能でした。この場を借りてお二人にも感謝を申し上げたいと思います。

Discussion

Evex 000Evex 000

short-handなのかは分かりませんがtypoしてませんかね?

import { Box, Heading, css } from "@kuma-ui/core"

function App() {
  return (
    <Box as="main" display="flex" flexDir={["column", "row"]}>
      <Heading className={css`font-sze: 24px;`} >
                              ^^^^^^^^
        Kuma UI
      </Heading>
    </Box>
  );
}