はじめに
Webアプリにテーマ選択機能を入れようとすると、最初に悩むのは「どこに色を持たせるか」です。
とりあえず背景色だけを変えればいいのでは?と思うかもしれませんが、実際には次のような要素があるので、テーマは1色ではなく複数の色セットとして扱うのが自然です。
- ボタン
- リンク
- 背景
- サブパネル
- 強調色
- フォーカスやボーダー
- 文字色
テーマ選択機能を「最初の1歩から作れる形」で整理します。
チャポットでテーマ選択を試してみてください。そこでテーマ切り替えのイメージをつかんだうえで、読み進めると理解しやすくなります。
- チャポット: https://chapot.net/
まずは最終的な処理の流れをイメージしておくと理解しやすいです。
ユーザーがテーマを選ぶ
-> テーマ名を保存する
-> 保存済み設定を読み込む
-> テーマ定義から色を取り出す
-> CSS変数に反映する
-> ページ全体の見た目が切り替わる
「1色」ではなく「役割ごとの色セット」を持つ
まず最初にやることは、「どんな色をまとめて管理するか」を決めることです。
例えばこの実装では、テーマを次のような7つの色のセットとして定義しています。
- リンク色
- ボタン色
- チャット/メイン背景色
- パネル背景色
- 受信吹き出し背景色
- 送信吹き出し背景色
- タイトル背景色
window.CHAPOT_THEME_COLOR = {
'blue_2': {
'color_1': '#2f9fb3',
'color_2': '#72c6d0',
'color_3': '#e4f7f8',
'color_4': '#c5e9ed',
'color_5': '#ffffff',
'color_6': '#75c9d3',
'color_7': '#eaf8f8',
}
};
これは「テーマ名と1つのカラーコード」の関係ではなく、
テーマ名 -> 色の役割セット
という設計です。中身を1色にすると、ボタンやリンク、背景が色とズレて見えづらくなりますが、役割ごとに色を持っておくと一貫性が出ます。
この考え方はチャット画面に限らず、以下のようなUIでも有効です。
- 管理画面のヘッダー / バナー / ボタン / 背景
- ダッシュボードのカード / アクセントカラー / 表示用背景
- サイト全体のブランドカラーと補助色
テーマを「色の役割ごとの辞書」にすることで、UIを微調整しやすくなります。
テーマを識別子として保存する設計
テーマの保存では、まず「色そのもの」を保存するのではなく、「どのテーマを選んだか」を保存する設計を使うと簡単です。
この実装では、カラーコードそのものをDBに入れるのではなく、テーマ識別子を保存しています。
if (option_name == "theme_color") {
message += "<option value=\"\">ブルーグレー</option>";
message += "<option value=\"blue_original\">ビビッドブルー</option>";
message += "<option value=\"blue_2\">ペイルブルー</option>";
message += "<option value=\"red_1\">レッド</option>";
message += "<option value=\"custom_color\">カスタムテーマ</option>";
}
保存対象は blue_2 や red_1 のようなテーマ名です。実際の色自体は別の定義で持ちます。
option_name = theme_color
option_value = blue_2
この方式のメリットは以下の通りです。
- 保存データが短くなる
- 既存のデータと互換性を保ちやすい
- 複数のテーマを追加しやすい
- 画面表示時に値から色配列を解決できる
もし「#2f9fb3」などの実色そのものを保存する設計にすると、テーマの見直しや再設計のたびにデータの更新が必要になります。一方で識別子ベースなら、テーマ定義だけを調整すれば済みます。
実際に実装する流れは、次のように考えるとわかりやすいです。
<select>で選択
-> 値を保存
-> 再読み込み時に値を取得
-> themeName から色定義を解決
-> CSSに反映
保存された設定を読み込む
ユーザーの選択を保存した後、ページ読み込み時に設定を再読込します。
read_option(login_container_id);
change_theme();
read_option() は保存済みのオプションを option オブジェクトへ格納します。ここではデータベースと画面との橋渡し役になっています。
function read_option(container_id = "") {
// Ajaxで設定を取得
// JSONをパース
// option[option_name] = option_value
}
この設計は一般的なWebアプリでも使えます。ポイントは、テーマの値をそのままDOMに埋め込むのではなく、まず state として保持することです。
const themeState = {
themeName: 'blue_2'
};
このように状態を持っておくと、画面更新時の再現性が上がります。
ここでいう「state」は、HTMLの中に直接閉じ込めるのではなく、JavaScript側で管理する変数のことです。これがあると、テーマを切り替えたあとに同じ処理を繰り返して再描画しやすくなります。
CSS変数へ反映してUI全体を切り替える
保存したテーマ識別子を使って、そのテーマの色セットを取り出し、CSS変数へ反映します。
switch (option.theme_color) {
case 'custom_color':
theme_color_1 = option.theme_custom_1 || THEME_COLOR['default']['color_1'];
break;
default:
theme_color_1 = THEME_COLOR[option.theme_color || 'default']['color_1'];
break;
}
document.documentElement.style.setProperty('--chat-theme-color-1', theme_color_1);
document.documentElement.style.setProperty('--chat-theme-color-2', theme_color_2);
document.documentElement.style.setProperty('--chat-theme-color-3', theme_color_3);
CSS側では、条件分岐を増やすのではなく、変数を参照します。
:root {
--theme-link: #2f9fb3;
--theme-button: #72c6d0;
--theme-bg: #e4f7f8;
}
.button {
background: var(--theme-button);
}
.link {
color: var(--theme-link);
}
.panel {
background: var(--theme-bg);
}
これが重要です。テーマ切り替えは「CSSの値を差し替える」よりも、「CSS変数の値を更新する」設計にすると、部品ごとに個別の実装を増やさずに済みます。
この考え方は、最初の実装としては少しだけ抽象的に感じますが、実際にはかなり簡単です。変数の値を更新して、それをCSSから参照するだけなので、各部品のスタイルを個別に書き換えるよりもかなり管理しやすくなります。
カスタムテーマで個別色を上書きする
多くのアプリは、プリセットだけでは不十分で、ユーザーに自分好みのカラーを選ばせたいと考えます。その実装としてよく使われるのが、「カスタムテーマ」という考え方です。
if (option.theme_color != "custom_color") {
// theme_color を custom_color に切り替える
}
// theme_custom_1 〜 theme_custom_7 を保存
この例では、最初に theme_color を custom_color に切り替え、その後で個別のカスタム色を保存しています。保存後に read_option() と change_theme() を実行することで、すぐに画面へ反映されます。
これにより、プリセットとカスタムを同じ流れで扱えます。
- プリセット: テーマ名に対応する色セットを選ぶ
- カスタム: 7項目それぞれに色を指定する
このような設計を使うと、ユーザーには簡単な選択メニューを見せつつ、開発者側ではきちんと構造化されたデータとして管理できます。
最初の実装では「テーマ一覧の定義」と「変数への反映」を用意しておけば十分で、その後にカスタムテーマを足していく流れが作りやすいです。
画面ごとに異なる部品をどう扱うか
テーマを選ぶとき、同じ変数でも画面によって使い方が違うケースがあります。
たとえば、チャットの背景色と、一般ページの背景色は同じ色を使う場合もあれば別色を使う場合もあります。ここで重要なのは、「変数名を画面ごとに分ける」ことです。
document.documentElement.style.setProperty('--page-bg', theme_color_3);
document.documentElement.style.setProperty('--page-link', theme_color_1);
document.documentElement.style.setProperty('--page-accent', theme_color_2);
このように、共通のテーマカラーをそのまま使い、ページごとにどこに割り当てるかを決める設計が汎用的です。
つまり、テーマは「色の原材料」であり、各画面はその中から自分の目的に合わせて使う構造になります。
ここが重要なのは、画面ごとに違う変数を用意するのではなく、共通のテーマ定義を用意してから、各画面で必要な変数だけを参照する考え方を持つことです。
汎用設計としてのポイント
1. 色そのものを保存しない
テーマ値には識別子だけを保存し、実色は定義側で持つと、更新や再設計がしやすくなります。
2. 役割ごとに色を分ける
背景・ボタン・リンク・強調色・タイトル色など、役割ごとにCSS変数を持つと、デザイン変更がしやすくなります。
3. CSS変数を中心に考える
テーマ切り替えはJavaScriptからDOMを一つずつ書き換えるより、CSS変数を更新する設計が簡潔です。
4. フォールバックを担保する
未設定時や古い値が残っているときに default を使うことで、ユーザー体験が安定します。
5. 保存 → 読み込み → 反映 を一本化する
設定を保存したら、read_option() で再取得し、change_theme() で反映する流れが最も理解しやすいです。これが一般的なテーマ管理の基本パターンです。
6. はじめは最小構成から始める
最初から完全なテーマシステムを作ろうとすると負担が大きいですが、まずは次の3点だけ用意すれば十分です。
- テーマ定義オブジェクト
- 選択したテーマ名を保持する変数
- CSS変数に反映する関数
ここが揃えば、後からカスタムテーマやサブテーマを足していけます。
まとめ
この実装の本質は「テーマを色そのものではなく、役割ごとの色セットとして扱うこと」です。
そして、保存の仕組みは次のように整理できます。
- ユーザーがテーマを選ぶ
- テーマ識別子を保存する
- 保存済み設定を再読込する
- 色辞書からテーマの7色を解決する
- CSS変数へ反映する
- 画面全体にすぐに適用する
この設計は、チャットUIだけではなく、一般的なHTML / JavaScript / CSSのアプリに広く使えます。
特に以下の場面で有効です。
- 設定画面があるWebアプリ
- 管理者画面のテーマ切替
- ダッシュボードやCMSの見た目変更
- モバイルアプリのようなWebビューのテーマ対応
テーマ選択機能を作りたいときは、まず次の順番で考えると実装しやすくなります。
- どんな色が必要かを決める
- テーマ名と色セットを定義する
- 選択値を保存する
- 保存値から色を取り出す
- CSS変数へ反映する
- 必要ならカスタムテーマを足す
最初の1歩としては、これだけで十分に実用的なテーマ切り替えが作れます。
このリポジトリの例は、その設計思想をかなりわかりやすく示してくれます。特に、色を「値」ではなく「役割」ベースで管理している点は、汎用的な実装の参考になります。
window.CHAPOT_THEME_COLOR = {
'blue_2': {
'color_1': '#2f9fb3', // リンク
'color_2': '#72c6d0', // ボタン
'color_3': '#e4f7f8', // チャットの背景
'color_4': '#c5e9ed', // ポップアップの背景
'color_5': '#ffffff', // 受信吹き出し
'color_6': '#75c9d3', // 送信吹き出し
'color_7': '#eaf8f8', // チャットタイトル
}
};
実際の定義は js/config.js にあり、default、blue_original、red_1、monotone_1 などのプリセットが登録されています。
ここで重要なのは、画面側が色の値を直接持たないことです。画面が保持するのは blue_2 のような識別子であり、実際の色は共通のテーマ定義から解決します。
この方式には次の利点があります。
- 設定値が短く、データベースに保存しやすい
- 色の組み合わせをプリセットとして管理できる
- 色を変更しても設定画面や保存形式を変更せずに済む
- 同じテーマ定義を通常画面と埋め込みチャットで共有できる
設定画面でテーマを選ぶ
管理画面から「テーマ(色)」を開くと、js/common.js の option_setting() が呼ばれます。
テーマ設定では、theme_color 用の select 要素を生成し、プリセットの値を option として並べています。
if (option_name == "theme_color") {
message += "<option value=\"\">ブルーグレー</option>";
message += "<option value=\"blue_original\">ビビッドブルー</option>";
message += "<option value=\"blue_2\">ペイルブルー</option>";
message += "<option value=\"red_1\">レッド</option>";
// ...
message += "<option value=\"custom_color\">カスタムテーマ</option>";
}
先頭のブルーグレーは value="" です。後のテーマ解決処理では空文字を default として扱うため、初期テーマを明示的な識別子なしで表現できます。
ユーザーが select を変更すると、変更イベントから選択値を取得します。
$(".option_setting_select").change(function() {
option_value = $(this).val();
// theme_color として保存する
});
選択値をサーバーへ保存する
選択値は containerOptionUpdate.php へ AJAX で送信されます。
url: CHAT_URL + 'containerOptionUpdate.php'
+ '?container_id=' + encodeURIComponent(login_container_id)
+ '&option_name=' + encodeURIComponent(option_name)
+ '&option_value=' + encodeURIComponent(option_value)
サーバー側では、同じ container_id と option_name の行を検索します。既存行があれば UPDATE、なければ INSERT を実行します。テーマ選択の場合、保存される組み合わせは次のようになります。
option_name = theme_color
option_value = blue_2
container_id = 利用中のコンテナID
色そのものではなくプリセット名を保存しているため、データベースの値からテーマの意図を読み取りやすい構造です。
なお、リクエスト値は SQL エスケープと HTML エスケープを通して保存されています。URLパラメータで送信するため、クライアント側では encodeURIComponent() も使われています。
保存したテーマをページへ反映する
ページ読み込み時、js/common.js の先頭で次の処理が行われます。
read_option(login_container_id);
read_option() は containerOptionGet.php から設定一覧を取得し、結果をグローバルな option オブジェクトへ格納します。したがって、テーマ反映処理はデータベースを直接参照せず、まずメモリ上の設定を読みます。
設定取得後、change_theme() が呼ばれます。
change_theme();
この関数は、option.theme_color を見て処理を分岐します。
switch (option.theme_color) {
case 'custom_color':
// theme_custom_1 ~ theme_custom_7 を使う
break;
default:
// THEME_COLOR[option.theme_color || 'default'] を使う
break;
}
プリセットの場合は、例えば次のように7色を取り出します。
theme_color_1 = THEME_COLOR[option.theme_color || 'default']['color_1'];
theme_color_2 = THEME_COLOR[option.theme_color || 'default']['color_2'];
theme_color_3 = THEME_COLOR[option.theme_color || 'default']['color_3'];
空文字や未設定値を default に置き換えているため、設定が存在しない場合でもデフォルトテーマへフォールバックできます。
取り出した色は、document.documentElement にCSSカスタムプロパティとして設定されます。
document.documentElement.style.setProperty('--chat-theme-color-1', theme_color_1);
document.documentElement.style.setProperty('--chat-theme-color-2', theme_color_2);
document.documentElement.style.setProperty('--chat-theme-color-3', theme_color_3);
// color_4 ~ color_7 も同様に設定
CSS側では、たとえばリンクやボタンを次のように定義しています。
.chat_body a {
color: var(--chat-theme-color-1);
}
.chat_body .btn-primary {
background-color: var(--chat-theme-color-2);
border-color: var(--chat-theme-color-2);
}
.chat_body {
background: var(--chat-theme-color-3);
}
この構造により、JavaScriptがCSS変数の値を更新するだけで、複数のUI部品をまとめて切り替えられます。テーマごとにクラスを付け替えたり、各要素を個別に検索して色を変更したりする必要はありません。
change_theme() はチャット部分だけでなく、ページ用の --page-bg、--page-link、--page-accent なども更新します。さらに、meta[name="theme-color"] が存在すれば、ブラウザやタブのテーマカラーもメインカラーに合わせます。
選択直後の反映
select の変更後、保存 AJAX が成功すると次の処理が実行されます。
read_option(login_container_id);
if (option_name == "theme_color") {
change_theme();
}
ここで一度設定を読み直してから反映している点がポイントです。画面上の一時的な値だけを使わず、保存処理後の設定を基準にしているため、次回読み込み時と同じ経路でテーマを再現できます。
トップページで表示中のチャットについては、PCまたはiPadの場合に setChatDialog() も呼ばれます。テーマ変更後のチャットダイアログを再生成し、表示状態へ反映するためです。
カスタムテーマへの切り替え
テーマ選択には、プリセット以外に「カスタムテーマ」もあります。カスタムテーマでは、次の7項目を個別に指定できます。
- リンクの色
- ボタンの色
- チャットの背景色
- ポップアップの背景色
- 受信吹き出しの背景色
- 送信吹き出しの背景色
- チャットタイトルの背景色
例えば、theme_custom_3 はチャットの背景色、theme_custom_7 はタイトル背景色です。
カスタム色の入力欄で色を変更すると、まず theme_color が custom_color でなければ、その値を custom_color に変更します。その後、変更された theme_custom_n を保存し、read_option() と change_theme() を実行します。
if (option.theme_color != "custom_color") {
// theme_color = custom_color を保存
}
// theme_custom_n の値を保存
// 保存成功後にテーマを再読み込みして反映
つまり、カスタム色を1項目変更した時点でプリセットからカスタムテーマへ移行します。カスタム値が未設定の項目については、change_theme() がデフォルトテーマの対応色をフォールバックとして使用します。
埋め込みチャットでの利用
チャットポップアップは iframe を使って表示されます。親ページ側の js/chat-popup.js では、ダイアログ生成時に option.theme_color を確認し、iframe の背景色とダイアログタイトルの背景色を決めています。
switch (option.theme_color) {
case 'custom_color':
color_iframe_bg = option.theme_custom_3
? option.theme_custom_3
: window.CHAPOT_THEME_COLOR['default']['color_3'];
color_dialog_title = option.theme_custom_7
? option.theme_custom_7
: window.CHAPOT_THEME_COLOR['default']['color_7'];
break;
default:
color_iframe_bg = window.CHAPOT_THEME_COLOR[
option.theme_color || 'default'
]['color_3'];
color_dialog_title = window.CHAPOT_THEME_COLOR[
option.theme_color || 'default'
]['color_7'];
}
js/config.js は window.CHAPOT_THEME_COLOR としてテーマ定義を公開しており、必要に応じて従来の THEME_COLOR 名にも割り当てています。このため、共通画面とポップアップ側で同じ色定義を共有できます。
この実装から学べる設計上のポイント
1. 設定値と表示値を分離する
データベースには blue_2 のような識別子だけを保存し、具体的なカラーコードはコード側のテーマ定義で管理しています。設定データと表示仕様が混ざらないため、テーマ追加や色調整がしやすくなります。
2. CSSカスタムプロパティを反映の境界にする
JavaScriptは「どのテーマを使うか」を決め、CSSは「その色をどの部品に使うか」を決めています。この責務分担によって、テーマ変更のロジックと見た目のルールを分離できます。
3. フォールバックを各所に持たせる
未設定時は default、カスタム値が空の時も default の対応色を使います。設定が不完全でも画面が空色にならないため、初期データや古い設定が残るケースに強い実装です。
4. iframeの外側も同じテーマで初期化する
iframe内部のCSSだけを変更すると、iframeの読み込み中に周囲のダイアログと色がずれることがあります。親ページ側でも背景色とタイトル色を決めておくことで、表示開始時の違和感を抑えています。
まとめ
このWebアプリのテーマ選択は、次の4段階に分かれています。
- 設定画面でテーマ識別子を選ぶ
-
containerOptionUpdate.php経由でコンテナ設定へ保存する -
read_option()で保存値をoptionに読み込む -
change_theme()がテーマ定義をCSSカスタムプロパティへ反映する
テーマを1色ではなく7色の組として定義し、JavaScriptとCSSの境界をCSSカスタムプロパティに置いたことで、プリセットとカスタムテーマの両方を同じ反映処理で扱えています。
テーマ切り替えを実装するときは、まず「保存する値はテーマ名か、色そのものか」「色の役割をどう分解するか」「再読み込み時にどの経路で復元するか」を決めると、画面とデータの責務を整理しやすくなります。
