テーマ切替
stimeo--theme
light/dark/system の選択を永続化し、ルートへ適用する。
stimeo--theme コントローラは light / dark / system の選択を localStorage に永続化し、system のあいだは OS の prefers-color-scheme に追従し、実効テーマをルートへ書き込んで利用側 CSS に渡します — 配色は持たず、状態フックだけを提供します。正規契約は 3 値の radiogroup で(system を提供するなら必ずこちら)、各選択肢は role="radio" + aria-checked +ロービング tabindex を持ち、矢印キーで移動・選択します(APG radio)。単一ボタン(aria-pressed)の 2 値構成は light↔dark 専用で、system は押下/非押下のトグルでは表現できないため扱いません。実効テーマ(light/dark)を data-theme と color-scheme で target(既定 html)へ付与し、フォーカスは移動せず、matchMedia を監視して system が OS 変更に追従します。stimeo--theme:change を発火し、matchMedia リスナは disconnect(Turbo 遷移含む)で解除します。初回描画の FOUC 対策は <head> の小さなインラインスニペットで行い、本コントローラの責務ではありません。ライブラリは挙動のみを提供し、配色は data-theme を見るこの Playground の CSS が持ちます。
プレビュー
このボックスだけローカルにテーマ適用されます — ページ自体は変わりません。
キーボード操作
| キー | 動作 |
|---|---|
| → / ↓ | 次のテーマ選択肢へ移動して選択する(radiogroup)。 |
| ← / ↑ | 前のテーマ選択肢へ移動して選択する。 |
| Home / End | 最初 / 最後の選択肢を選択する。 |
| Enter / Space | フォーカス中の選択肢を実行する(単一ボタンでは切替)。 |
<%# Markup for the theme / color-scheme toggle demo.
The controller persists the light/dark/system choice, follows the OS while in
system, and writes data-theme + color-scheme onto the target. To avoid theming the
whole Playground, this demo targets a local preview element instead of <html>, so
the preview box below reacts to the resolved theme while the page is unaffected. %>
<div class="theme-demo">
<div class="theme-demo__switcher" data-controller="stimeo--theme"
data-stimeo--theme-target-value="#theme-demo-preview"
data-stimeo--theme-storage-key-value="stimeo-theme-demo"
data-stimeo--theme-mode-value="system"
role="radiogroup" aria-label="<%= t("components.theme.demo.aria_label") %>">
<button type="button" class="theme-demo__option"
data-stimeo--theme-target="option" role="radio"
data-action="click->stimeo--theme#set" data-stimeo--theme-mode-param="light">
<%= t("components.theme.demo.light") %>
</button>
<button type="button" class="theme-demo__option"
data-stimeo--theme-target="option" role="radio"
data-action="click->stimeo--theme#set" data-stimeo--theme-mode-param="dark">
<%= t("components.theme.demo.dark") %>
</button>
<button type="button" class="theme-demo__option"
data-stimeo--theme-target="option" role="radio"
data-action="click->stimeo--theme#set" data-stimeo--theme-mode-param="system">
<%= t("components.theme.demo.system") %>
</button>
</div>
<div id="theme-demo-preview" class="theme-demo__preview">
<h3 class="theme-demo__preview-title"><%= t("components.theme.demo.preview_title") %></h3>
<p><%= t("components.theme.demo.preview_text") %></p>
</div>
</div>
/*
* Presentation-only styles for the theme toggle demo.
* The library writes data-theme (resolved light/dark) and color-scheme onto the
* preview element and keeps aria-checked in sync; this CSS owns the palette keyed
* off data-theme — exactly the consumer's job. No colors ship in the library.
*/
.theme-demo {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.theme-demo__switcher {
display: inline-flex;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
width: fit-content;
}
.theme-demo__option {
padding: 0.4rem 0.8rem;
border: 0;
border-radius: 0.375rem;
background: transparent;
cursor: pointer;
font-size: 0.9rem;
}
/* The library marks the selected option with aria-checked="true". */
.theme-demo__option[aria-checked="true"] {
background: var(--color-primary);
color: var(--white);
}
/* The preview reacts to the resolved theme the library applied. */
.theme-demo__preview {
padding: 1.25rem;
border-radius: 0.75rem;
border: 1px solid var(--border-default);
background: var(--surface-card);
color: var(--color-text);
}
.theme-demo__preview[data-theme="dark"] {
border-color: var(--border-strong);
background: var(--slate-900);
color: var(--slate-200);
}
.theme-demo__preview-title {
margin: 0 0 0.5rem;
font-size: 1.05rem;
}
// Consumer-side JS for the theme toggle demo (demo-only).
// The controller persists the choice and applies data-theme on its own; this only
// logs the change event to show the contract (mode = chosen, resolved = effective).
document.addEventListener("stimeo--theme:change", (event) => {
console.log(`[theme] mode=${event.detail.mode} resolved=${event.detail.resolved}`);
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--theme"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
option
|
mode パラメータを持つ radiogroup の選択肢(role="radio")。 |
data-stimeo--theme-target="option" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
mode
|
現在の選択: light / dark / system(既定 system)。 |
data-stimeo--theme-mode-value |
storageKey
|
永続化に使う localStorage キー(既定 stimeo-theme)。 |
data-stimeo--theme-storage-key-value |
target
|
状態フックの付与先要素のセレクタ(既定 html)。 |
data-stimeo--theme-target-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
set
|
mode アクションパラメータから明示的にモードを選択する。 |
stimeo--theme#set |
toggle
|
2 値の単一ボタン契約で light↔dark を切り替える。 |
stimeo--theme#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
テーマ変化時に発火。detail に mode と resolved を伴う。 |
stimeo--theme:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-theme |
target 要素(既定 html) | 実効テーマ: "light" か "dark"。 |
color-scheme |
target 要素 | 実効テーマに合わせたネイティブ UI の配色ヒント。 |
aria-checked |
radiogroup の選択肢 | 選択中は "true"、他は "false"。 |
aria-pressed |
単一トグルボタン(2 値) | 実効テーマが dark のとき "true"。 |