最下部追従
stimeo--stick-to-bottom
新しい行が届くと下へ追従します。読み返している最中は追いかけません。
チャットやログのように下へ伸びていく領域を、新しい行に合わせて追従させます。ただし追いかけるのは、すでにいちばん下の近くにいるときだけです。上へ戻って読み返している最中に、勝手に下へ引っ張られることはありません。読み返している間に届いた行は、新着があることとして知らせます。「新着へ」のようなボタンから、いちばん下へ戻ることもできます。追従しているかどうかは、主張ではなく実際のスクロール位置から決めているので、どこかで食い違うことがありません。
実行中
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Stick-to-bottom demo: the scrollable log follows new messages while you're at the
bottom, but holds and shows the "new messages" jump button if you've scrolled up. This
catalog has no Action Cable / Turbo Stream, so the Add button appends a <li> — exactly
the mutation a Turbo Stream broadcast would make — and the controller reacts. The jump
button lives inside the log (a descendant, wired with a plain data-action) and CSS
reveals it only while data-has-new. pin-on-connect starts the log at the bottom so the
first message is followed rather than flagged — a fresh scroll container renders at the
top, and the controller does this on every connect, which a once-per-document page event
cannot. The library only follows/flags and reflects data-pinned / data-has-new; demo.css
owns the look.
The log scrolls but its only control — the jump button — is hidden until there is
something to jump to, so most of the time a keyboard user cannot reach it at all
(WCAG 2.1.1). Rather than hand-write a tab stop, compose stimeo--scroll-area over it:
that controller exists for exactly this and gives the viewport tabindex="0" only while
it holds no rendered focusable content, so the tab stop appears and disappears in step
with the jump button instead of doubling up with it. It goes on a wrapper because
stick-to-bottom must sit on the scrolling element itself, and it takes the log as its
viewport target. role="region" comes free with the label below. %>
<div class="stb-demo">
<div class="stb-demo__log-wrap" data-controller="stimeo--scroll-area">
<div
class="stb-demo__log"
data-controller="stimeo--stick-to-bottom"
data-stimeo--stick-to-bottom-behavior-value="smooth"
data-stimeo--stick-to-bottom-pin-on-connect-value="true"
data-stimeo--scroll-area-target="viewport"
aria-label="<%= t("components.stick_to_bottom.demo.viewport_label") %>">
<ul class="stb-demo__messages" data-stimeo--stick-to-bottom-target="content">
<% (1..8).each do |i| %>
<li><%= t("components.stick_to_bottom.demo.message") %> <%= i %></li>
<% end %>
</ul>
<button
type="button"
class="stb-demo__jump"
data-action="click->stimeo--stick-to-bottom#scrollToBottom">
<%= t("components.stick_to_bottom.demo.jump") %>
</button>
</div>
</div>
<button
type="button"
class="demo-trigger"
data-stb-demo-add
data-message-label="<%= t("components.stick_to_bottom.demo.message") %>">
<%= t("components.stick_to_bottom.demo.add") %>
</button>
</div>
/*
* Presentation-only styles for the stick-to-bottom demo. The library follows/flags new
* content and reflects data-pinned / data-has-new; this CSS gives the log a fixed height
* so it scrolls, and reveals the floating jump button only while data-has-new is set.
*/
.stb-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 24rem;
align-items: flex-start;
}
/* Wrapper for the composed stimeo--scroll-area (it needs an element of its own, and
stick-to-bottom must stay on the scrolling log). Layout-neutral. */
.stb-demo__log-wrap {
width: 100%;
}
.stb-demo__log {
position: relative;
width: 100%;
height: 9rem;
overflow: auto;
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.5rem;
}
.stb-demo__messages {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stb-demo__messages li {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background: var(--surface-subtle);
}
/* The "new messages" button floats at the bottom, shown only while scrolled up. */
.stb-demo__jump {
position: sticky;
bottom: 0;
display: none;
margin-left: auto;
padding: 0.25rem 0.625rem;
border: 0;
border-radius: 999px;
background: var(--color-primary);
color: var(--white);
cursor: pointer;
}
.stb-demo__log[data-has-new] .stb-demo__jump {
display: block;
}
// Stick-to-bottom demo (consumer-side JS).
//
// No Action Cable / Turbo Stream here, so the Add button appends a <li> to the log —
// the same mutation a broadcast would make — and the controller follows it (while pinned)
// or flags it (while scrolled up). The starting scroll position is not wired here: the
// log declares pin-on-connect, so it comes up at the bottom on its own.
document.querySelectorAll(".stb-demo").forEach((root) => {
const list = root.querySelector('[data-stimeo--stick-to-bottom-target="content"]');
const add = root.querySelector("[data-stb-demo-add]");
if (!list || !add) return;
let count = list.children.length;
const label = add.dataset.messageLabel ?? "Message";
add.addEventListener("click", () => {
const li = document.createElement("li");
li.textContent = `${label} ${(count += 1)}`;
list.appendChild(li);
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--stick-to-bottom"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
content
|
追加を監視する要素(無ければスクロール領域自身)。 | data-stimeo--stick-to-bottom-target="content" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
threshold
|
pinned とみなす末尾からの距離(px、既定 80)。 | data-stimeo--stick-to-bottom-threshold-value |
behavior
|
通常時の追従スクロール挙動(auto / smooth。reduced-motion 下は instant に固定)。 |
data-stimeo--stick-to-bottom-behavior-value |
pinOnConnect
|
connect 時に最下部へ移動し、到達できていれば pinned で開始する(既定 false)。この初期ジャンプは behavior に関わらず常に即時。 |
data-stimeo--stick-to-bottom-pin-on-connect-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
scrollToBottom
|
最下部へジャンプする(「新着」ボタン用)。data-has-new は要求時点で下ろし、data-pinned は実際に到達した位置から導出する。 | stimeo--stick-to-bottom#scrollToBottom |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
pin
|
pinned 状態が変化したとき発火。detail.pinned を伴う。 |
stimeo--stick-to-bottom:pin |
new
|
unpinned 中に新着が来たとき発火。detail.count を伴う。 |
stimeo--stick-to-bottom:new |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-pinned |
コントローラ要素 | 最下部追従中に付与(true)。 |
data-has-new |
コントローラ要素 | 上方閲覧中に新着が来たとき付与(true)。 |