Overflow Indicator
stimeo--overflow-indicator
Detects remaining scroll room and exposes data-overflow-start / data-overflow-end.
The stimeo--overflow-indicator controller is a state-detection utility with no APG widget. It watches the viewport's scroll position and size (via the wired scroll action, a LayoutObserver for resize, and a MutationObserver for content changes) and syncs data-overflow-start / data-overflow-end so consumer CSS can draw edge shadows or arrows for "more content this way". Optional page buttons scroll one viewport at a time (scrollByPage) and have their disabled state synced to the matching direction's remaining room; it dispatches stimeo--overflow-indicator:change. data-overflow-* carry no ARIA semantics. Behavior only — shadows and arrows are owned by this Playground; all observers and listeners are released on disconnect (Turbo included), and scrollByPage honors prefers-reduced-motion.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Activate a scroll button (standard button behavior; optional). |
<%# Markup for the overflow-indicator (horizontal-scroll affordance) demo.
The library exposes the viewport's remaining scroll room via data-overflow-start /
data-overflow-end and syncs the prev/next buttons' disabled to that room. The
shadow/arrow look is drawn by demo.css. %>
<div class="overflow-demo" data-controller="stimeo--overflow-indicator"
data-stimeo--overflow-indicator-orientation-value="horizontal">
<button
type="button"
class="overflow-demo__nav"
aria-label="<%= t("components.overflow_indicator.demo.prev") %>"
data-stimeo--overflow-indicator-direction-param="start"
data-action="click->stimeo--overflow-indicator#scrollByPage">‹</button>
<div class="overflow-demo__viewport"
data-stimeo--overflow-indicator-target="viewport"
data-action="scroll->stimeo--overflow-indicator#update"
tabindex="0"
role="region"
aria-label="<%= t("components.overflow_indicator.demo.label") %>">
<% 10.times do |i| %>
<div class="overflow-demo__card"><%= t(
"components.overflow_indicator.demo.card", number: i + 1
) %></div>
<% end %>
</div>
<button
type="button"
class="overflow-demo__nav"
aria-label="<%= t("components.overflow_indicator.demo.next") %>"
data-stimeo--overflow-indicator-direction-param="end"
data-action="click->stimeo--overflow-indicator#scrollByPage">›</button>
</div>
/* Presentation CSS for overflow-indicator. The library syncs data-overflow-start / -end
and the buttons' disabled, so the edge shadow (mask) and button look are drawn here. */
.overflow-demo {
display: flex;
align-items: center;
gap: 0.5rem;
max-width: 460px;
}
/* Prev/next buttons. The direction with no room becomes disabled, tied to data-overflow-*. */
.overflow-demo__nav {
flex: 0 0 auto;
width: 2rem;
height: 2rem;
border: 1px solid var(--border-strong);
border-radius: 50%;
background: var(--surface-card);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
color: var(--color-text);
}
.overflow-demo__nav:disabled {
opacity: 0.35;
cursor: default;
}
.overflow-demo__nav:focus-visible {
outline: 2px solid var(--color-primary-hover);
outline-offset: 2px;
}
/* The viewport is the horizontally-scrolling container; it reads data-overflow-* to mask
its edges. */
.overflow-demo__viewport {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding: 0.5rem;
scroll-behavior: smooth;
/* Edge gradient mask. Only the side with remaining room fades out. */
--mask-start: transparent;
--mask-end: transparent;
-webkit-mask-image: linear-gradient(
to right, var(--mask-start), var(--ink) 12%, var(--ink) 88%, var(--mask-end)
);
mask-image: linear-gradient(
to right,
var(--mask-start),
var(--ink) 12%,
var(--ink) 88%,
var(--mask-end)
);
}
.overflow-demo__viewport:focus-visible {
outline: 2px solid var(--color-primary-hover);
outline-offset: 2px;
}
.overflow-demo__viewport[data-overflow-start="true"] {
--mask-start: transparent;
}
.overflow-demo__viewport[data-overflow-start="false"] {
--mask-start: var(--ink);
}
.overflow-demo__viewport[data-overflow-end="true"] {
--mask-end: transparent;
}
.overflow-demo__viewport[data-overflow-end="false"] {
--mask-end: var(--ink);
}
.overflow-demo__card {
flex: 0 0 auto;
width: 110px;
height: 80px;
display: grid;
place-items: center;
border-radius: 8px;
background: var(--color-primary-hover);
color: var(--white);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.overflow-demo__viewport {
scroll-behavior: auto;
}
}
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--overflow-indicator"
Targets
| Name | Description | Attribute |
|---|---|---|
viewport
required
|
The scrollable element whose remaining scroll room is measured. | data-stimeo--overflow-indicator-target="viewport" |
Values
| Name | Description | Attribute |
|---|---|---|
orientation
|
The scroll axis to measure, horizontal or vertical (default horizontal). |
data-stimeo--overflow-indicator-orientation-value |
threshold
|
The px slack from each edge before it counts as scrollable (default 1). | data-stimeo--overflow-indicator-threshold-value |
Actions
| Name | Description | Action |
|---|---|---|
scrollByPage
|
Scrolls one viewport page toward the direction param (start/end), honoring reduced motion. | stimeo--overflow-indicator#scrollByPage |
update
|
Re-measures remaining scroll room and reflects the state hooks; wired to the viewport's scroll. | stimeo--overflow-indicator#update |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched when scrollability transitions; detail carries start and end booleans. | stimeo--overflow-indicator: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 |
|---|---|---|
data-overflow-start |
Viewport | "true" when there is room to scroll toward the start (left / top). |
data-overflow-end |
Viewport | "true" when there is room to scroll toward the end (right / bottom). |