Overflow Indicator
stimeo--overflow-indicator
Says whether there is more to scroll to, so CSS can draw the edge shadows or arrows.
Watches a scrollable area for whether there is more before it and after it. Both are readable from CSS, so an edge shadow or an arrow can say "there is more this way". It looks again not only as you scroll but when the size changes and when the content is replaced. Buttons that move by one screenful can sit alongside it, and the one with nowhere to go stops being pressable. Visitors who ask for less motion get no smooth scrolling. The shadows and arrows themselves are drawn by this demo's CSS.
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;
/* 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;
/* vital-700, not the hover step: white on vital-600 measures 4.26 — just under
the floor — and this pairing has to hold in both themes, so it cannot follow
a theme-aware token. */
background: var(--vital-700);
color: var(--white);
font-weight: 600;
}
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). |