ReactとCSSの新しいUIフレームワークを作った
リンク
公式サイト
デモサイト
概要
Camome UI は React または HTML + CSS で使用できる UI フレームワークです。
ソースは React + CSS Modules で書かれており、 それを React コードからインポートするか、静的なクラス名で出力された CSS ファイルを読み込むことで使用できます。
特徴
軽量なバンドルサイズ
- 例えば
@mui/material@5.11.6
のButton
は単体で16.9KBあり、それに Emotion や ThemeProvider も加わります。 - それに対して Camome UI の
Button
は単体で 1.42KB(CSS 含む)、それにグローバル CSS の 4KB が加わります(計測方法)。
これらのフレームワークは言ってみれば「フルスタック」であり、どんなウェブサイト・アプリでも構築できる柔軟性と機能を持っています。しかしそれらすべてを必要とするサイトは多くないはずです。Camome UI はすべてをカバーするよりも軽量化を重視しています。
@camome/core
は CSS Modulesでスタイリングされており、NPM で配布しているパッケージ内で *.module.scss
を import
しています。つまり、バンドラーを経由しないと使用することができません。しかし Next.js であれば next.config.js
で { transpilePackages: ["@camome/core"]}
を設定するだけだし、Vite は最初からトランスパイルしてくれるので、そんなに面倒ではないと思います。
この設計の理由は、全コンポーネントの CSS を一括で読み込みたくないからです。JavaScript コードもツリーシェイキング可能なので、import したコンポーネントのコードのみがバンドルされるようにしてあります。
高い汎用性とカスタマイズ性
軽量さを重視していると言っても、実際のサイトの実装で使いにくかったら意味がありません。
Camome UI では solid
, soft
, outline
, ghost
を variant
として定義してあり、各コンポーネントが実装(継承?)しています。<Button variant="soft" />
のように Prop を渡すことで切り替えられます。
また、 colorScheme
によってセマンティックな命名の色を選択できます。これはすべての variant
に適用できます。
カスタムテーマとダークモード
そしてこれらの色やその他のデザイントークンはすべてカスタマイズ可能です。camome.config.js
をプロジェクトルートに作成し、CLI コマンドを実行することでカスタムテーマの CSS ファイルを出力できます。
// camome.config.js
import { defineConfig } from "@camome/system";
// 型補完が有効です
export default defineConfig({
themes: {
common: {
color: {
primary: {
// カスタムのプライマリカラー
0: "#faf5ff",
1: "#f3e8ff",
// ...
// `soft` variantに使用する色を変更
// コールバック関数を渡すことで他のトークンを参照可能
soft: {
bg: (get) => lighter(get("color.primary.0")),
},
},
},
},
// `light`, `dark` のそれぞれでも設定可能
light: {},
dark: {},
},
});
npx camome theme --output ./styles/custom-theme.css
デザイントークンは 以下のような CSS 変数として出力され、 @camome/core
から使用されています。また、これらをアプリケーション内のスタイリングにも使用することで、デザインに一貫性を持たせられたり、上述の variant
のスタイルを継承できたり、ダークテーマに自動で対応できたりといった効用が得られます。
@layer cmm.theme {
:root[data-theme="light"] {
--cmm-color-primary-font: var(--cmm-color-primary-7);
--cmm-color-primary-emphasis: var(--cmm-color-primary-6);
/* ... */
}
}
テーマの切り替えは <html>
要素に data-theme="<light または dark>"
属性を付与することで実現できます。
<html data-theme="<light または dark>">
<!-- ... -->
</html>
デザイントークンの中でも Semantic な色を使用することで、[data-theme="dark"] & {...}
のようなダークテーマ用のスタイルを大量に書く必要がなくなります。多くの場合は以下のような CSS を書くだけで自動的に light と dark に対応できます。
.card {
border: 1px solid var(--cmm-color-border-base);
background: var(--cmm-color-surface-1);
h2 {
color: var(--cmm-color-font-base);
}
p {
color: var(--cmm-color-font-subtle);
}
}
@layer
によるスタイルの確実なオーバーライド
CSS Cascade Layersをサポートしているため、詳細度を気にすることなくオーバーライドが可能です。👇 以下の宣言が最初にインポートされればあとは面倒なことを考えなくても済みます。
/* @camome/system/dist/theme.css */
@layer cmm.reset, cmm.theme, cmm.base, cmm.components;
これに関しては Bootstrap などの既存のフレームワークでも、@import(bootstrap.css) layer(bootstrap);
のようにレイヤーを宣言することは可能なようです。
フォーム用ユーティリティ
input
系コンポーネントは label
や error
などの props を受け取ると、自動で id
を発行して label
要素や aria-describedby
属性を適切に紐付けます。Web 標準以外の挙動は行わないので、react-hook-formのようなライブラリとの相性はかなりいいと思います。
詳しくはフォームを参照してください。
// react-hook-form
const { register } = useForm();
return (
<form>
<TextInput
label="Name"
placeholder="Your name..."
error={errors.firstName?.message}
{...register("name", {
required: errMsg.required,
})}
/>
<RadioGroup label="Job title" aria-required orientation="horizontal">
<Radio label="Developer" value="developer" {...register("jobTitle")} />
<Radio label="Other" value="other" {...register("jobTitle")} />
</RadioGroup>
<Checkbox
label="Agree to Privacy Policy"
{...register("privacy", {
required: errMsg.required,
})}
error={errors.privacy?.message}
/>
<Button type="submit" variant="soft">
Submit
</Button>
</form>
);
アクセシブル
@camome/core
ではイベントハンドラーや useEffect
を使用していませんが、キーボードでの操作やその他アクセシビリティを保証できるように実装しています。Switchのようなコンポーネントも CSS と HTML だけで実装されていますが、ちゃんとフォーカスできるようにしてあります。
JavaScript が必須になるような複雑なコンポーネント(例: Menu)は、Headless UI のような外部ライブラリと接続して使うための「ヘッドオンリー」コンポーネントとして提供しています。しかし利便性のためには @camome/core/headlessui
のようなディレクトリを切って公式実装を配布したほうがいいかなとも思っています。
まとめ
よければぜひ使ってみてください。
GitHub リポジトリにスターしていただけるとうれしいです 🙏
[2023-02-18] ⭐️122 ありがとうございます!
Discussion
UIがめっちゃ素敵ですね!
開発過程も知りたいです!