Slider
import { useState } from 'react'
import {
RangeSlider,
Slider,
Surface,
Text,
} from '@language-lit/material3-expressive'
export function SliderExample() {
const [volume, setVolume] = useState(0.62)
const [price, setPrice] = useState<readonly [number, number]>([20, 80])
return (
<Surface
as="section"
aria-labelledby="slider-example-title"
color="surface-container-low"
shape="extra-large"
className="slider-example"
>
<Text
as="h2"
id="slider-example-title"
variant="titleLarge"
emphasis="emphasized"
>
Sliders
</Text>
<Text as="p" variant="bodyMedium">
Continuous, stepped, centered, range, and vertical source paths.
</Text>
<label className="slider-example__field">
<Text as="span" variant="labelLarge">
Volume · {Math.round(volume * 100)}%
</Text>
<Slider
value={volume}
onValueChange={setVolume}
name="volume"
/>
</label>
<label className="slider-example__field">
<Text as="span" variant="labelLarge">
Reading speed
</Text>
<Slider min={0.5} max={2} steps={5} defaultValue={1} />
</label>
<label className="slider-example__field">
<Text as="span" variant="labelLarge">
Centered balance
</Text>
<Slider min={-1} max={1} steps={7} defaultValue={0.25} centered />
</label>
<label className="slider-example__field" dir="rtl">
<Text as="span" variant="labelLarge">
RTL stepped
</Text>
<Slider
aria-label="RTL stepped slider"
steps={4}
defaultValue={0.4}
/>
</label>
<div
className="slider-example__field"
role="group"
aria-label="Price range example"
>
<Text as="span" variant="labelLarge">
Price · ${price[0]}–${price[1]}
</Text>
<RangeSlider
startAriaLabel="Minimum price"
endAriaLabel="Maximum price"
min={0}
max={100}
steps={9}
value={price}
onValueChange={setPrice}
/>
</div>
<div className="slider-example__vertical-row">
<div className="slider-example__vertical-field">
<Text as="span" variant="labelMedium">
Top to bottom
</Text>
<Slider
aria-label="Top to bottom level"
orientation="vertical"
defaultValue={0.35}
/>
</div>
<div className="slider-example__vertical-field">
<Text as="span" variant="labelMedium">
Bottom to top
</Text>
<Slider
aria-label="Bottom to top level"
orientation="vertical"
topToBottom={false}
defaultValue={0.65}
/>
</div>
<div className="slider-example__vertical-field">
<Text as="span" variant="labelMedium">
Disabled
</Text>
<Slider
aria-label="Unavailable level"
orientation="vertical"
defaultValue={0.5}
disabled
/>
</div>
</div>
</Surface>
)
}Sliderは1つの値を選び、RangeSliderは順序付きの値の組を選びます。どちらもアクセシブルな値の意味付け、フォーカス、フォーム、ラベル、無効状態にはネイティブのrange入力を使い、Materialのトラック、つまみ、目盛り、停止位置の表示は装飾として描画します。
import {
RangeSlider,
Slider,
} from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
<label>
Volume
<Slider name="volume" defaultValue={0.4} />
</label>
<Slider
aria-label="Reading speed"
min={0.5}
max={2}
steps={5}
defaultValue={1}
/>
<RangeSlider
aria-label="Price range"
startAriaLabel="Minimum price"
endAriaLabel="Maximum price"
min={0}
max={100}
defaultValue={[20, 80]}
startInputProps={{ name: 'minimum' }}
endInputProps={{ name: 'maximum' }}
/>値の仕様
- 制御する場合は
valueとonValueChangeを使います。非制御の場合はdefaultValueを指定し、必要に応じてコールバックを渡します。 minとmaxの既定値はそれぞれ0と1です。値はこの範囲内に制限されます。stepsは両端の間にある選択可能な値の数です。たとえば0…10の範囲でsteps={4}を指定すると、0, 2, 4, 6, 8, 10を選べます。ポインター操作では最も近い目盛りに合わせ、ちょうど中間の場合は固定されたソースに従って小さい側を選びます。ネイティブのアクセシビリティ値操作ではソースのsemanticsループにある別の同点処理を維持し、大きい側を選びます。- 操作が完了したタップ、ドラッグ、処理済みキーの解放、またはアクセシビリティ値操作の後に
onValueChangeFinishedが1回呼び出されます。値の保存には完了コールバックではなくonValueChangeを使ってください。 RangeSliderは順序が逆の初期値を並べ替え、2つのつまみが交差しないようにします。ポインターは近い方のつまみを選びます。つまみが完全に重なっている場合、その前方を押すと開始側、それ以外を押すと終了側を選びます。これはソースと同じです。
id、name、form、required、ARIA関連付け、ネイティブの変更/フォーカスイベントなどの入力属性はSliderに渡せます。refは入力を参照し、classNameとstyleは視覚上のルートに適用されます。RangeSliderでは通常のDOM propsとrefはrole="group"のルートに適用されます。startInputProps/endInputPropsおよびstartInputRef/endInputRefで2つの入力を個別に指定できます。
向き、方向、キーボード
Sliderの既定の向きは横です。orientation="vertical"はソースの現行VerticalSlider経路に対応します。topToBottomの既定値はtrueで、最小値が上になります。下から上へ増やすにはfalseにします。固定されたソースには縦方向の範囲スライダーAPIがないため、RangeSliderは常に横向きです。
横方向の値の順序は論理方向に従い、最小値はインライン開始側にあります。そのためRTLでは物理的なトラックとLeft/Rightキーによる増減が反転します。Homeで最小値、Endで最大値を選びます。連続スライダーの矢印キー増分はソースに従い1%、段階式スライダーでは1目盛りです。Page Up/Downでは最大10目盛り移動します。ソースはRTLでも横方向のPageキーを反転しないため、この非対称な動作を維持しています。縦方向のUp/DownとPageキーはtopToBottomに従います。
ポインター操作ではソースのスロップ動作を維持します。タップでは離した時に最初の押下座標を確定します。同じ軸に動かすとドラッグになり、タッチを直交方向に動かした場合はページスクロールに任せます。無効な入力にはフォーカスも変更もできません。
トラック、つまみ、状態
横方向のルートは親のインライン幅いっぱいに広がり、最低48pxの操作領域を確保します。縦方向のルートは既定で200×48pxで、通常のCSSでサイズを変更できます。この領域内の寸法は次のとおりです。
- トラックの厚さは16pxで、外側の角は8px、つまみ側の角は2pxです。
- 横方向の既定のつまみは
4×44pxです(縦方向では44×4px)。 - フォーカス、押下、ドラッグ時はつまみの主軸方向の厚さを2pxに半減します。
- つまみの端からトラックまでは6px空けます。
- 内側のフォーカス表示では隣の隙間に4px加えますが、どちらのつまみも移動しません。
- 目盛りと両端の停止インジケーターは直径4pxの円です。
単一スライダーにcenteredを指定すると、幾何学的な中心から値までをアクティブな進捗として描画します。範囲のアクティブ部分は2つのつまみの間です。AndroidXと同じく、離散値の中間目盛りとつまみは外側の角丸の内側に配置し、端点はトラックの全幅を使います。通常の単一スライダーでは非アクティブな遠端だけに停止位置を描画します。中央基準と範囲表示では、該当するセグメントがある場合に非アクティブな両端を描画します。
thumb、startThumb、endThumb、trackContent、renderTick、renderStopIndicatorはトラック内に配置される受動的な視覚スロットで、aria-hiddenの配下に置かれます。ネイティブ入力を置き換えたり、別の操作を追加したりすることはできません。showStopIndicator={false}はソースで停止インジケーターのレンダラーがnullの場合に対応します。
アクセシビリティ、フォーム、SSR
意味を持つ各つまみは<input type="range" role="slider">です。ラベルの関連付け、数値のmin/max/now状態、フォーム送信、リセット、無効動作、独立したTab位置はブラウザーが提供します。Sliderではラッパーのラベル、label for、aria-label、aria-labelledbyを使えます。RangeSliderではローカライズされたstartAriaLabelとendAriaLabelが必須で、動的なアクセシブル上限/下限はもう一方のつまみ位置までに制限されます。
装飾用トラックにはaria-hiddenを設定します。:focus-visibleでは各つまみの周囲にトークン由来のリングを描画し、強制カラーにも対応します。SSRとハイドレーションで決定的なマークアップを生成し、実行時スタイルは注入しません。
トークンとソースの境界
視覚的な値にはすべてMaterial3Providerのスコープ内で設定される--m3e-comp-slider-*カスタムプロパティを使います。既定値は、ソースで目盛り色が交差する仕様、無効時のつまみ色をsurface上に事前合成する仕様、無効トラックの不透明度がそれぞれ異なる仕様を保ちます。
実装はAndroidX Material 3リビジョン225f50d42bf0adeb2abf4b6109befb5ab6ce4efcに固定されています。生成されたSliderTokensのうちSlider.ktが文字どおり参照する15個だけをソースに基づく解決経路として扱います。未参照の36個も実行可能な台帳に記録し、定義されていない動作を作りません。Composeのmodifier、interaction source、canvas scope、state holder、非推奨の互換オーバーロードはプラットフォーム上の仕組みとして適応または対象外とし、観測可能な出力はコンポーネント準拠記録とADR 0031で扱います。