個人開発の画面の色とCSSを、どのサービスでも同じ形で管理する
技術スタックの記事で、画面のスタイルは「Tailwind CSS v4 を基本に、色・フォント・角丸などをサービスごとに @theme で定義する」と決めました。
ただ、それだけでは、実際に画面を作るときにどこに何を書くかが決まっていませんでした。
1つ目のサービス「ハコヅミ」の画面を見直すのに合わせて、これから作るサービスすべてに共通する、色とCSSの管理のしかたを決めたので、その中身と理由をまとめます。 (2026年9月時点の方針です)
困っていたこと
Tailwind CSS は、HTMLのクラスに bg-emerald-700 や text-gray-600 のように、見た目を直接書いていく道具です。
手軽ですが、そのまま画面を増やしていくと、次のことが起きます。
- 同じ見た目が、あちこちに散らばる。「保存」ボタンを作るたびに
rounded bg-emerald-700 px-4 py-2 text-whiteと書くことになる - 色を変えたいとき、全部のファイルを直すことになる。イメージカラーを緑から青に変えるだけで、何十か所も書き換える
- 少しずつずれていく。あるページは
gray-600、別のページはgray-500、といった小さな違いが積もって、統一感がなくなる
1人で長く保守していくには、「色を変えたいときに、1か所を直せば済む」形にしておきたいと考えました。
3段構えにする
そこで、スタイルを次の3段に分けて持つことにしました。
| 段 | 置き場 | 何をまとめるか |
|---|---|---|
| ① デザイントークン | src/app.css の @theme |
色・フォント・角丸などの値に、意味のある名前を付ける |
| ② UI部品 | src/lib/components/ui/ |
ボタン、入力欄、見出し、カードなど、何度も出てくる部品 |
| ③ 共通クラス | src/app.css の @utility |
部品にするほどでもない組み合わせ。数は最小限 |
① デザイントークン:値に名前を付ける
Tailwind CSS v4 では、@theme に書いた値が、そのままクラス名として使えるようになります。
@theme {
--color-primary: #2f5d4a; /* 基本の色 */
--color-surface: #f6f3ea; /* ページの背景 */
--color-ink: #2b2a26; /* 文字 */
--color-danger: #b3261e; /* 削除・エラー */
--radius-box: 0.5rem; /* 角の丸み */
}
こう書くと、bg-primary、text-ink、rounded-box のようなクラスが使えます。
画面には bg-emerald-700(どんな色か)ではなく、bg-primary(何のための色か)と書くわけです。色を変えたくなったら、@theme の値を1つ直すだけで、全画面の色が変わります。
② UI部品:見た目と動きをまとめる
ボタンや入力欄のように何度も出てくるものは、Svelteのコンポーネントにします。
<Button type="submit">保存</Button>
<Button variant="danger">削除</Button>
ボタンの角丸や余白、ホバーしたときの色は、Button の中にだけ書きます。画面の側は「主のボタン」「削除のボタン」と種類を選ぶだけです。
用意する部品は、ボタン、リンクのボタン、入力欄、選択欄、チェックボックス、エラーの表示、画面の見出し、見出し付きのまとまり、カードくらいです。
③ 共通クラス:最小限に
「ページの幅と余白」のように、部品にするほどではないけれど毎回同じ組み合わせになるものだけ、@utility で共通のクラスにします。
@utility page {
margin-inline: auto;
max-width: 42rem;
padding: 1rem;
}
ここは増やさないのが肝心です。Tailwind には @apply で独自のクラスを作る方法もありますが、これを量産すると、結局は昔ながらのCSSと同じになってしまいます。まとめたいものがあれば、まず②の部品にできないかを考えます。
決めたルール
3段構えを守るために、次のルールを決めました。
- ページやサービス固有の部品では、パレットの色(
emerald-700、gray-600など)を直接書かない。トークンか、UI部品を使う - 余白や配置など、その画面だけのレイアウトは、直接 Tailwind で書いてよい。全部を部品にしようとすると、かえって読みにくくなる
- サービス固有の部品(ハコヅミなら、ギアの入力欄一式など)は、UI部品を組み合わせて作る
守られているかは、CIで確かめる
「パレットの色を直接書かない」は、うっかり破りやすいルールです。人の目で毎回確かめるのは大変なので、パレットの色が書かれていたらCIが失敗するようにしました。
小さなスクリプトで、画面のファイルから bg-emerald-700 や text-white のようなクラスを探し、見つかったら一覧を出して失敗させます。トークンと部品を定義する場所(app.css と ui/)だけは対象外です。
依存パッケージは増やさず、Lintの一部として動かしています。
AIに実装を任せていると、手癖で text-gray-600 などを書いてくることがあります。ルールを文章で伝えるだけでなく、機械で止めるようにしておくと安心です。
サービスごとに変えるのは、トークンの「値」だけ
この方針で一番大事にしたのは、次の点です。
トークンの名前と、UI部品の作りは、どのサービスでも同じにする。サービスごとに変えるのは、トークンの値だけ。
たとえば、ハコヅミは「森」をイメージした深い緑が primary ですが、次のサービスでは別の色を primary に入れます。名前は同じ primary です。
| トークン | ハコヅミ | 次のサービス(例) |
|---|---|---|
primary |
深い緑 | そのサービスの色 |
surface |
生成り | そのサービスの背景 |
radius-box |
0.5rem | 丸め・角ばり、好みで |
こうしておくと、
- UI部品は、2つ目のサービスにそのまま持っていける(中身は
bg-primaryなどの名前でしか色を知らない) - どのサービスのコードを読んでも、
text-ink-mutedは「補足の文字」だと分かる。70代になって複数のサービスを行き来しても迷わない - 見た目の個性は、トークンの値でしっかり出せる
家計簿アプリでは、見た目が決まっている部品集をそのまま使ったため、業務システムっぽい画面になりました。今回は、部品の作りは共通、見た目の個性は値でという分け方にしています。
名前の付け方
トークンの名前は、「何色か」ではなく「何に使うか」で付けます。
| 用途 | 名前 |
|---|---|
| 基本の色 | primary、primary-hover、primary-soft、on-primary(primary の上の文字) |
| 差し色 | accent |
| 背景 | surface(ページ)、surface-raised(カードなど一段上の面) |
| 文字 | ink、ink-muted |
| 線 | line |
| 削除・エラー | danger、on-danger |
| 角丸 | radius-box |
文字と線は、最初は素直に text・border と名付けるつもりでした。ところが Tailwind ではクラス名が text-text・border-border になってしまい、読みにくいので、ink(インク)と line(線)にしました。
まだ決めていないこと
- フォント:当面は端末に入っているフォント(システムフォント)を使います。Webフォントは、サービスの雰囲気が固まってきたら見直します
- ダークモード:トークンの値を切り替えれば対応できる形にはなっていますが、まだ作りません
- 共通部分の切り出し:UI部品やトークンの名前は、2つ目のサービスを作るときに、実際に使えたものだけを共通として切り出します。1つ目の段階で先回りはしません
おわりに
- スタイルは「デザイントークン・UI部品・最小限の共通クラス」の3段で持つ
- 画面にはパレットの色を直接書かない。CIで機械的に止める
- サービスごとに変えるのはトークンの値だけ。名前と部品の作りは共通にする
実際にハコヅミの画面をこの方針で作り直した話は、ハコヅミを作る #11に書きました。