Skip to content
fandhe-frontendcore v0.4.3
GitHub

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 の最小例:

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 自体はスタイルを持ちません。

How do I center a div?
Use display: flex and align-items: center.
New reply while you were scrolled up.

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-more

API Reference

Arguments

NameTypeDefaultDescription
root: props.stuckMessageScrollerStuckMessageScrollerStuck::Bottom最下部追従の状態(bottom/free)。data-stuck へ出力する(message_scroller.rs)。
root: props.has_newboolfalsetrue のとき data-has-new 存在属性を付与する(新着メッセージの表示のみ、検知ロジックは内包しない、message_scroller.rs)。
viewport: label&str""空でなければ role="region" + aria-label へ出力する(名前のない region を作らない、message_scroller.rs)。
jump_to_latest: visiblebool(なし・必須)true で data-visible 存在属性、false で hidden 属性を出力する(自動連動しない 2 択、message_scroller.rs)。
jump_to_latest: label&str""空でなければ aria-label へ出力する(message_scroller.rs)。
load_more: loadingbool(なし・必須)true のとき data-loading 存在属性を付与する(message_scroller.rs)。
load_more: disabledbool(なし・必須)true のときネイティブ disabled + data-disabled を付与する。loading と自動連動しない(message_scroller.rs)。

Data Attributes

PartAttributeObserved Values
jump-to-latestdata-visible(値なし)
load-moredata-disabled(値なし)
load-moredata-loading(値なし)
rootdata-has-new(値なし)
rootdata-stuckbottom, free

Examples

Basic conversation container

既定状態(data-stuck="bottom")で会話ログを viewport に収める基本例です。

How do I center a div?
Use display: flex and align-items: center.

Loading older messages

load-more を loading/disabled 状態で表示し、履歴読み込み中を実演する例です。

Earlier reply.

New message while scrolled up

stuck=free + has-new + jump-to-latest 可視で、離脱中に新着が届いた状態を実演する例です。

New reply while you were scrolled up.

自前 CSS の最小例

data-scope/data-part/data-stuck/data-visible 属性セレクタでスタイルを当てる例です。

data-scope / data-part / data-stuck / data-visible 属性セレクタで viewport の高さとスクロール、jump-to-latest の表示切り替えを当てる最小例です。headless-ui 自体はスタイルを持ちません。

Styled with plain CSS.
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;
}

Accessibility

Keyboard Interactions

KeyDescription
Tab / Shift+Tabviewport は tabindex="0" を固定出力しているため、通常のタブ順で到達・離脱できる(WAI 慣行に従い矢印キー/Page キーでフォーカス済み要素をスクロールできる、message_scroller.rs)。
(なし)root/content/anchor はキー操作を提供しない静的コンテナである。jump-to-latest/load-more はネイティブ button であり Enter/Space が既定で作動する(message_scroller.rs)。

WAI-ARIA

AttributeDescription
role="region" / aria-labelviewport パーツに、label が空でないときのみ固定付与する(名前のない region を作らない、message_scroller.rs)。
aria-hidden="true"anchor パーツに固定付与する(純粋な計測用マーカーのため、message_scroller.rs)。
aria-labeljump-to-latest パーツに、label が空でないときのみ出力する(message_scroller.rs)。
(role="log" / aria-live / aria-busy は付与しない)ストリーミング通知・応答待ちの読み上げはアプリ責務のため本モジュールは付与しない(message.rs と同じ判断軸、message_scroller.rs「aria-live/aria-busy を付けない理由」節)。