Message Scroller
AI チャット UI の会話ログを収めるスクロールコンテナを表現する shadcn/ui Message Scroller 相当の部品です(参照軸はイシュー #2001)。fandhe-frontend-headless-ui の message_scroller mod が構造・アクセシビリティ(WAI-ARIA)・表示状態(data-*)のみを提供する unstyled 部品で、root / viewport / content / anchor / jump_to_latest / load_more の 6 anatomy パーツを持ちます。Message と同じく状態機械を持たない静的部品です。
責務境界
最下部追従・新着検知・スクロール位置の計測や復元は一切内包しません。SSR は常に「最下部に居る」初期状態(data-stuck="bottom")を決定的に描画し、実行時のスクロール位置計測・自動追従・新着到着の検知は fandhe-frontend-wasm-full 側の配線が担います(本部品自体の責務ではありません)。
root は stuck(bottom / free)・has_new(bool)の props を受け取り、それぞれ data-stuck / data-has-new として出力します。data-has-new は存在属性(値が真のときのみ出力)です。jump_to_latest の実際の可視判定(stuck=free かつ has_new 等の組み合わせ)は呼び出し側または配線層が行い、本部品は visible: bool を受け取って data-visible(可視時)/ hidden(非可視時、JS 無効時に「押しても何も起きないボタン」を見せないため)を切り替えるだけです。
viewport は Scroll Area の viewport と同じ tabindex="0" 固定契約を持ちますが、message-scroller scope 自身のパーツとして独立に実装しています(scroll_area へは委譲しません)。label を渡すと role="region" + aria-label を出力します(空文字列のときは省略し、名前のない region を作りません)。カスタムスクロールバーが必要な場合は scroll_area::scrollbar / scroll_area::thumb を viewport の中へ入れ子にできます(両 scope は独立して共存できます)。
content はスロットで、role="log" を固定付与しません(ストリーミング通知の読み上げタイミングはアプリケーション固有の UX 判断であり、Message と同じ判断軸で責務外としています)。anchor は viewport 末尾に置く aria-hidden="true" の計測用センチネルで、配線層が最下部到達の観測対象として使います。
load_more は loading / disabled の 2 つの bool を受け取り、それぞれ独立に data-loading 存在属性 / ネイティブ disabled + data-disabled を出力します(自動連動しません)。aria-busy は付与しません。
アクセシビリティ
viewportはtabindex="0"を固定付与します(WAI 慣行に従い矢印キー・Page キーでフォーカス済み要素をスクロールできるため)。labelが非空のときのみrole="region"+aria-labelを出力します。anchorはaria-hidden="true"を固定付与します。jump_to_latest/load_moreはネイティブbutton(type="button")で、Enter・Space が既定で作動します。aria-live/aria-busy/aria-posinset/aria-setsizeはいずれも付与しません。通知タイミング・総数(無限履歴では SSR 時点で確定できません)はアプリケーション固有の判断であり、本部品の責務外です。
fandhe-frontend-pre-styled-ui に対応するスタイル済み部品があります。Themes 版は Message Scroller を参照してください。
自前 CSS の最小例:
[data-scope="message-scroller"][data-part="viewport"] {
overflow-y: auto;
height: 12rem;
}
[data-scope="message-scroller"][data-part="jump-to-latest"] {
position: sticky;
bottom: 0.5rem;
}
[data-scope="message-scroller"][data-part="jump-to-latest"]:not([data-visible]) {
display: none;
}関連 API: fandhe-frontend-headless-ui API
Demo
枠線・余白は docs サイト側のデモ枠であり、fandhe-frontend-headless-ui 自体はスタイルを持ちません。
Features
- 会話ログのスクロールコンテナを表現する 6 anatomy パーツ(root/viewport/content/anchor/jump-to-latest/load-more)を提供する(message_scroller.rs)。message.rs と同じく状態機械を持たない静的部品であり、最下部追従・新着検知・スクロール位置の計測・復元は一切内包しない(wasm-full 側の配線は #2122)。
- root は data-stuck(bottom/free)・data-has-new(存在属性)を出力する。SSR は常に「最下部に居る」初期状態(既定 bottom)を決定的に描画する(message_scroller.rs「data-stuck/data-has-new」節)。
- viewport は scroll_area::viewport と同じ tabindex="0" 固定契約を持つが、message-scroller scope 自身のパーツとして独立実装する(scroll_area へは委譲しない、message_scroller.rs「viewport は message-scroller scope 自身のパーツ」節)。label が非空のときのみ role="region" + aria-label を出力する。
- content は role="log" を固定付与しない純スロット(ストリーミング通知はアプリ責務、message.rs と同じ判断軸)。anchor は viewport 末尾に置く aria-hidden="true" の計測用センチネル(wasm-full 側の観測対象、#2122)。
- jump-to-latest は visible=false のとき hidden 属性を出力し JS 無効時に「押しても何も起きないボタン」を見せない。load-more は loading/disabled が自動連動しない独立した 2 引数で、aria-busy は付与しない。参照実体は shadcn/ui の Message Scroller のみ(ark-ui・chakra-ui・Radix に対応部品なし、docs/design/component-coverage-map.md §12.1、参照軸 #2001)。
Anatomy
root
viewport
content
anchor
jump-to-latest
load-moreAPI Reference
Arguments
| Name | Type | Default | Description |
|---|---|---|---|
root: props.stuck | MessageScrollerStuck | MessageScrollerStuck::Bottom | 最下部追従の状態(bottom/free)。data-stuck へ出力する(message_scroller.rs)。 |
root: props.has_new | bool | false | true のとき data-has-new 存在属性を付与する(新着メッセージの表示のみ、検知ロジックは内包しない、message_scroller.rs)。 |
viewport: label | &str | "" | 空でなければ role="region" + aria-label へ出力する(名前のない region を作らない、message_scroller.rs)。 |
jump_to_latest: visible | bool | (なし・必須) | true で data-visible 存在属性、false で hidden 属性を出力する(自動連動しない 2 択、message_scroller.rs)。 |
jump_to_latest: label | &str | "" | 空でなければ aria-label へ出力する(message_scroller.rs)。 |
load_more: loading | bool | (なし・必須) | true のとき data-loading 存在属性を付与する(message_scroller.rs)。 |
load_more: disabled | bool | (なし・必須) | true のときネイティブ disabled + data-disabled を付与する。loading と自動連動しない(message_scroller.rs)。 |
Data Attributes
| Part | Attribute | Observed Values |
|---|---|---|
jump-to-latest | data-visible | (値なし) |
load-more | data-disabled | (値なし) |
load-more | data-loading | (値なし) |
root | data-has-new | (値なし) |
root | data-stuck | bottom, free |
Examples
Basic conversation container
既定状態(data-stuck="bottom")で会話ログを viewport に収める基本例です。
Loading older messages
load-more を loading/disabled 状態で表示し、履歴読み込み中を実演する例です。
New message while scrolled up
stuck=free + has-new + jump-to-latest 可視で、離脱中に新着が届いた状態を実演する例です。
自前 CSS の最小例
data-scope/data-part/data-stuck/data-visible 属性セレクタでスタイルを当てる例です。
data-scope / data-part / data-stuck / data-visible 属性セレクタで viewport の高さとスクロール、jump-to-latest の表示切り替えを当てる最小例です。headless-ui 自体はスタイルを持ちません。
[data-scope="message-scroller"][data-part="viewport"] {
overflow-y: auto;
height: 12rem;
}
[data-scope="message-scroller"][data-part="jump-to-latest"] {
position: sticky;
bottom: 0.5rem;
}
[data-scope="message-scroller"][data-part="jump-to-latest"]:not([data-visible]) {
display: none;
}
Accessibility
Keyboard Interactions
| Key | Description |
|---|---|
| Tab / Shift+Tab | viewport は tabindex="0" を固定出力しているため、通常のタブ順で到達・離脱できる(WAI 慣行に従い矢印キー/Page キーでフォーカス済み要素をスクロールできる、message_scroller.rs)。 |
| (なし) | root/content/anchor はキー操作を提供しない静的コンテナである。jump-to-latest/load-more はネイティブ button であり Enter/Space が既定で作動する(message_scroller.rs)。 |
WAI-ARIA
| Attribute | Description |
|---|---|
| role="region" / aria-label | viewport パーツに、label が空でないときのみ固定付与する(名前のない region を作らない、message_scroller.rs)。 |
| aria-hidden="true" | anchor パーツに固定付与する(純粋な計測用マーカーのため、message_scroller.rs)。 |
| aria-label | jump-to-latest パーツに、label が空でないときのみ出力する(message_scroller.rs)。 |
| (role="log" / aria-live / aria-busy は付与しない) | ストリーミング通知・応答待ちの読み上げはアプリ責務のため本モジュールは付与しない(message.rs と同じ判断軸、message_scroller.rs「aria-live/aria-busy を付けない理由」節)。 |