Scroll Visibility
stimeo--scroll-visibility
Shows or hides an element based on scroll amount or direction (back-to-top, hide-on-scroll).
The stimeo--scroll-visibility controller has no dedicated APG pattern; when the element is a button it follows the Button practice. In offset mode the element is shown once the scroll source is scrolled past offset px; in direction mode it is hidden while scrolling down and shown while scrolling up. The window is observed by default, but in a fixed-shell layout where the page itself does not scroll, point root at a container selector to observe that container's scroll (and toTop then returns that container to the top). Visibility is reflected through the hidden attribute (so a hidden control also leaves the focus order) and data-state. The scroll listener is passive and coalesced through requestAnimationFrame, and removed on disconnect (Turbo included). toTop honors prefers-reduced-motion with an instant jump and can move focus to a focusSelector target. Behavior only — the look and transition are owned by this Playground.
Scroll inside this box — a “Back to top” button appears at the bottom-right.
Content line 1
Content line 2
Content line 3
Content line 4
Content line 5
Content line 6
Content line 7
Content line 8
Content line 9
Content line 10
Content line 11
Content line 12
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Activate the back-to-top button (standard button behavior). |
<%# Markup for the scroll-visibility (scroll-driven visibility toggle / GoToTop) demo.
The library toggles the target via hidden / data-state once the scroll source passes a
threshold, and toTop scrolls that source back to the top. Here the scroll source is the
demo's own scroll box (root points at it), so the demo is self-contained and works even
when the page itself does not scroll on the window. The look and transition are demo.css's.
Scroll inside the box and a "Back to top" button appears at the bottom-right. %>
<div
class="scroll-visibility-demo"
data-controller="stimeo--scroll-visibility"
data-stimeo--scroll-visibility-root-value="#scroll-visibility-demo-scroller"
data-stimeo--scroll-visibility-offset-value="120"
data-stimeo--scroll-visibility-mode-value="offset">
<div id="scroll-visibility-demo-scroller" class="scroll-visibility-demo__scroller"
tabindex="0" role="region" aria-labelledby="scroll-visibility-demo-hint">
<p id="scroll-visibility-demo-hint" class="scroll-visibility-demo__hint"><%= t(
"components.scroll_visibility.demo.hint"
) %></p>
<% 12.times do |i| %>
<p class="scroll-visibility-demo__line"><%= t(
"components.scroll_visibility.demo.line", number: i + 1
) %></p>
<% end %>
</div>
<button
type="button"
class="go-to-top"
hidden
data-stimeo--scroll-visibility-target="element"
data-action="click->stimeo--scroll-visibility#toTop">
<%= t("components.scroll_visibility.demo.to_top") %>
</button>
</div>
/* Presentation CSS for scroll-visibility. The library only toggles hidden / data-state
and does the toTop scroll, so visuals like the scroll box, positioning, and fade are
drawn here. The demo scrolls inside its own box (the controller's root) rather than the
window, so the "Back to top" button is pinned to the box, not the viewport. */
.scroll-visibility-demo {
position: relative; /* positioning context for the absolutely-pinned button */
color: var(--color-text-muted);
}
/* The scroll source the controller observes (data-...-root-value points here). */
.scroll-visibility-demo__scroller {
max-height: 16rem;
overflow-y: auto;
padding: 1rem;
border: 1px solid var(--border-default);
border-radius: 0.5rem;
background: var(--surface-subtle);
}
.scroll-visibility-demo__scroller:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.scroll-visibility-demo__hint {
margin: 0 0 0.5rem;
font-weight: 600;
}
.scroll-visibility-demo__line {
margin: 0.5rem 0;
}
/* "Back to top" button, pinned to the demo box. Visible only once the library removes
hidden (and the control is back in the focus order). */
.go-to-top {
position: absolute;
right: 1.25rem;
bottom: 1.25rem;
z-index: 1;
padding: 0.6rem 1rem;
border: none;
border-radius: 999px;
background: var(--color-primary-hover);
color: var(--white);
font-weight: 600;
cursor: pointer;
box-shadow: 0 6px 16px rgba(var(--vital-rgb), 0.4);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.go-to-top:hover {
transform: translateY(-2px);
}
.go-to-top:focus-visible {
outline: 2px solid var(--vital-900);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.go-to-top {
transition: none;
}
.go-to-top:hover {
transform: none;
}
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--scroll-visibility"
Targets
| Name | Description | Attribute |
|---|---|---|
element
required
|
The element shown or hidden via hidden/data-state based on scroll. |
data-stimeo--scroll-visibility-target="element" |
Values
| Name | Description | Attribute |
|---|---|---|
offset
|
The scroll threshold in px for showing (offset mode) or the top reveal zone (direction mode); default 400. | data-stimeo--scroll-visibility-offset-value |
mode
|
Visibility logic: offset (show past offset) or direction (show on scroll up); default offset. |
data-stimeo--scroll-visibility-mode-value |
focusSelector
|
An optional selector to move focus to after toTop scrolls (default empty). |
data-stimeo--scroll-visibility-focus-selector-value |
root
|
An optional selector for the scroll source container; defaults to the window. | data-stimeo--scroll-visibility-root-value |
Actions
| Name | Description | Action |
|---|---|---|
toTop
|
Scrolls the source to the top and optionally moves focus to focusSelector, honoring reduced motion. |
stimeo--scroll-visibility#toTop |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched when visibility transitions; detail carries the visible boolean. | stimeo--scroll-visibility:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
hidden |
Element | Hidden below the threshold (also removed from the focus order). |
data-state |
Root element | "hidden" / "visible". |