カラーピッカー
stimeo--color-picker
チャンネル別スライダーと 16 進入力が双方向に同期する。
stimeo--color-picker コントローラは、色の選択を 2 次元パレットではなく独立した WAI-ARIA Slider のチャンネル(色相・彩度・明度、任意でアルファ)に分解する。これによりどの調整もキーボードとスクリーンリーダーで操作できる(WCAG 1.4.1:色だけに依存しない)。各スライダーは aria-valuenow と、"Hue 210 degrees" のような人間可読な aria-valuetext を持つ。16 進入力は双方向に同期・正規化され、現在色は --stimeo-color(ここではプレビューの色見本を駆動)に公開され、hidden フィールドにも同期される。ポインタドラッグのリスナは AbortController に束ね、ドラッグ終了時と disconnect(Turbo 遷移含む)で解除する。色は 16 進値から完全に再構築できる。ライブラリは振る舞いのみを提供し、色見本やトラックの見た目はこの Playground の CSS(サムの位置は小さな消費側スクリプト)が担う。
キーボード操作
| キー | 動作 |
|---|---|
| ↑ / → | フォーカス中のチャンネルを 1 ステップ増やす。 |
| ↓ / ← | フォーカス中のチャンネルを 1 ステップ減らす。 |
| PageUp / PageDown | 大きいステップで増減する。 |
| Home / End | チャンネルの最小/最大へ。 |
<%# Markup for the color-picker demo.
The library manages each channel's (hue / saturation / lightness) role="slider"
value, composes them into a color two-way-synced with the hex input, and updates
--stimeo-color for the preview. The 2D palette is decomposed into independent 1D
sliders for accessibility. Thumb position and look are the consumer's CSS. %>
<div
class="color-picker"
data-controller="stimeo--color-picker"
data-stimeo--color-picker-value-value="#3366cc">
<div
class="color-picker__preview"
data-stimeo--color-picker-target="preview"
aria-hidden="true"></div>
<div class="color-picker__channels">
<% { hue: [t("components.color_picker.demo.hue"), 360, 210],
saturation: [t("components.color_picker.demo.saturation"), 100, 60],
lightness: [t("components.color_picker.demo.lightness"), 100, 50] }
.each do |channel, (label, max, now)| %>
<div class="color-picker__channel">
<span class="color-picker__channel-label"><%= label %></span>
<div
class="color-picker__track color-picker__track--<%= channel %>"
role="slider"
aria-label="<%= label %>"
data-channel="<%= channel %>"
tabindex="0"
aria-valuemin="0"
aria-valuemax="<%= max %>"
aria-valuenow="<%= now %>"
data-stimeo--color-picker-target="slider"
data-action="
keydown->stimeo--color-picker#onKeydown
pointerdown->stimeo--color-picker#onPointerDown">
<span class="color-picker__thumb" aria-hidden="true"></span>
</div>
</div>
<% end %>
</div>
<label class="color-picker__hex">
<span><%= t("components.color_picker.demo.hex") %></span>
<input
type="text"
inputmode="text"
aria-label="<%= t("components.color_picker.demo.hex") %>"
value="#3366cc"
data-stimeo--color-picker-target="hex"
data-action="change->stimeo--color-picker#onHexInput" />
</label>
<input type="hidden" name="brand_color" data-stimeo--color-picker-target="field" />
</div>
/*
* Presentation-only styles for the color-picker demo.
* The library updates each slider's aria-valuenow / aria-valuetext, the hex value,
* and --stimeo-color for the preview. The thumb position uses --stimeo-demo-pos,
* which demo.js normalizes from aria-valuenow (an example of consumer-side styling).
*/
.color-picker {
display: grid;
gap: 0.85rem;
max-width: 20rem;
}
.color-picker__preview {
height: 3rem;
border: 1px solid #cbd5e1;
border-radius: 0.5rem;
/* Show the current color the library exposes. */
background: var(--stimeo-color, #000);
}
.color-picker__channels {
display: grid;
gap: 0.7rem;
}
.color-picker__channel {
display: grid;
gap: 0.25rem;
}
.color-picker__channel-label {
font-size: 0.8rem;
color: #475569;
}
.color-picker__track {
position: relative;
height: 0.85rem;
border-radius: 0.5rem;
cursor: pointer;
touch-action: none;
}
.color-picker__track--hue {
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.color-picker__track--saturation {
background: linear-gradient(to right, #808080, #2563eb);
}
.color-picker__track--lightness {
background: linear-gradient(to right, #000, #2563eb, #fff);
}
.color-picker__thumb {
position: absolute;
top: 50%;
/* The fraction the demo's demo.js computed from aria-valuenow. */
left: var(--stimeo-demo-pos, 0%);
width: 0.95rem;
height: 0.95rem;
transform: translate(-50%, -50%);
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #334155;
background: transparent;
}
.color-picker__track:focus-visible {
outline: 2px solid var(--accent, #2563eb);
outline-offset: 3px;
}
.color-picker__hex {
display: grid;
gap: 0.25rem;
font-size: 0.8rem;
color: #475569;
}
.color-picker__hex input {
padding: 0.4rem 0.55rem;
border: 1px solid #cbd5e1;
border-radius: 0.4rem;
font: inherit;
font-family: ui-monospace, monospace;
}
// Demo that consumes the color-picker's values (consumer-side JS).
//
// The core controller (stimeo--color-picker) updates each slider's aria-valuenow /
// aria-valuetext, the hex value, and --stimeo-color for the preview, but does not
// provide the thumb position (the look). Here, on each change event, we normalize each
// slider's aria-valuenow to a fraction and reflect it as --stimeo-demo-pos for the
// thumb position (an example of consumer-side styling).
document.querySelectorAll('[data-controller~="stimeo--color-picker"]').forEach((picker) => {
const sliders = picker.querySelectorAll('[role="slider"]');
const positionThumbs = () => {
sliders.forEach((slider) => {
const now = Number(slider.getAttribute('aria-valuenow'));
const max = Number(slider.getAttribute('aria-valuemax')) || 1;
const min = Number(slider.getAttribute('aria-valuemin')) || 0;
const fraction = max > min ? (now - min) / (max - min) : 0;
slider.style.setProperty('--stimeo-demo-pos', `${fraction * 100}%`);
});
};
picker.addEventListener('stimeo--color-picker:change', positionThumbs);
positionThumbs();
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--color-picker"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
slider
必須
|
色相/彩度/明度/アルファのチャンネルスライダー(role=slider)。data-channel で識別される。 |
data-stimeo--color-picker-target="slider" |
hex
|
16 進カラーを保持するテキスト input。チャンネルと双方向同期する。 | data-stimeo--color-picker-target="hex" |
preview
|
--stimeo-color カスタムプロパティで現在色を受け取るスウォッチ要素。 |
data-stimeo--color-picker-target="preview" |
field
|
フォーム送信用に現在の 16 進カラーを反映する隠し input。 | data-stimeo--color-picker-target="field" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
value
|
モデルの初期値となる 16 進カラー(既定 #000000)。 |
data-stimeo--color-picker-value-value |
alpha
|
アルファチャンネルを有効にするか(既定 false。無効時は不透明を維持)。 | data-stimeo--color-picker-alpha-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onHexInput
|
確定時に hex input を解析し全チャンネル・表示を同期する。無効入力時は直前の有効値に戻す。 | stimeo--color-picker#onHexInput |
onKeydown
|
APG スライダーパターンに沿ってフォーカス中チャンネルを増減する(矢印、PageUp/Down、Home/End)。 | stimeo--color-picker#onKeydown |
onPointerDown
|
チャンネルスライダー上でポインタドラッグを開始し、移動を追跡して値を設定する。 | stimeo--color-picker#onPointerDown |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
色が変わるたびに発火。detail は { value, rgba }(16 進文字列と { r, g, b, a })。 |
stimeo--color-picker:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow |
スライダー | 各チャンネルの現在値。 |
aria-valuetext |
スライダー | 人間可読な値(例 "Hue 210 degrees")。 |
value |
16 進入力/フィールド | 現在色の 16 進表現。 |
--stimeo-color |
プレビュー/ルート | プレビュー色見本用の現在色。 |