カラーピッカー
stimeo--color-picker
色相、彩度、明度を別々のスライダーで選ぶ色選択。16 進の入力欄と双方向につながります。
色の選択を、2 次元のパレットではなく色相、彩度、明度の独立したスライダーに分けています。分けてあるので、どの調整もキーボードだけでできて、読み上げにも今の値が伝わります。読み上げは数値ではなく「色相 210 度」のような言い方で届きます。16 進の入力欄とは双方向につながり、どちらを触っても揃います。選んだ色は CSS からも読めるので、見本の塗りに使えます。フォームには隠しフィールドとして送られます。ドラッグ中に別の指が割り込んでも、値が飛んだり操作が途切れたりしません。選んだ色は 16 進の値だけから元どおりに組み立てられるので、ブラウザの戻るで最初の色に戻ってしまうことがありません。見本やトラックはこのデモの CSS で描いています。
キーボード操作
| キー | 動作 |
|---|---|
| ↑ / → | フォーカス中のチャンネルを 1 ステップ増やす。logicalTrack を立てた RTL では左右が入れ替わる。 |
| ↓ / ← | フォーカス中のチャンネルを 1 ステップ減らす。logicalTrack を立てた RTL では左右が入れ替わる。 |
| 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.
data-value-text carries the announced wording, so this catalog announces the
channel in the reader's own language instead of the library's English. %>
<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 %>"
data-value-text="<%= t("components.color_picker.demo.value_text.#{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 --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;
/* Was a hard-coded slate-600, which does not follow the theme: on the dark
canvas it measures 2.30:1. The muted token is theme-aware and clears AA. */
color: var(--color-text-muted);
}
.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(--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;
/* Theme-aware, for the same reason as the channel label above. */
color: var(--color-text-muted);
}
.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 and reconcile event, we
// normalize each slider's aria-valuenow to a fraction and reflect it as --demo-pos for
// the thumb position (an example of consumer-side styling). Both events are needed:
// change reports what the reader did, reconcile a color the controller settled on
// itself (a runtime value or alpha change), and the thumbs must follow either one.
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('--demo-pos', `${fraction * 100}%`);
});
};
picker.addEventListener('stimeo--color-picker:change', positionThumbs);
picker.addEventListener('stimeo--color-picker:reconcile', positionThumbs);
positionThumbs();
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--color-picker"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
slider
必須
|
色相/彩度/明度/アルファのチャンネルスライダー(role=slider)。data-channel で識別される。任意の data-value-text テンプレート({value} を差し込む)で読み上げ文言を決められる。 |
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)。確定した色をコントローラが書き戻すので、Turbo 復帰でもフォーム送信でも選んだ色が運ばれる。外部から書き換えるとモデルを組み直し reconcile を発火する。 |
data-stimeo--color-picker-value-value |
alpha
|
アルファチャンネルを有効にするか(既定 false。無効時は不透明を維持し、アルファスライダーも編集できない)。 | data-stimeo--color-picker-alpha-value |
logicalTrack
|
トラックを論理 CSS で組んでいて RTL でミラーするか(既定 false)。立てるとポインタ写像と横矢印の対が論理方向に従う。立てなければ direction は一切読まない。 | data-stimeo--color-picker-logical-track-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 |
reconcile
|
alpha または value の実行時変更で確定色が動いたときに発火。detail は change と同じ形。 | stimeo--color-picker:reconcile |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow |
スライダー | 各チャンネルの現在値。 |
aria-valuetext |
スライダー | 人間可読な値(data-value-text があればその文言、無ければ例 "Hue 210 degrees")。 |
aria-valuemin / aria-valuemax |
スライダー | 解決したチャンネルの範囲(マークアップの値、省略時はチャンネル既定)。 |
value |
16 進入力/フィールド | 現在色の 16 進表現。 |
--stimeo--color |
プレビュー/ルート | プレビュー色見本用の現在色。 |