Scroll Area
stimeo--scroll-area
Makes a scrolling box reachable from the keyboard and tells CSS where in it you are.
Makes an ordinary scrolling box easier to use, without drawing a scrollbar of its own. When the content overflows and there is nothing focusable inside, the box itself becomes focusable, so no region is left reachable only by a mouse wheel. Whether you are at the start, in the middle or at the end is readable from CSS for drawing edge shadows, along with whether it overflows at all and how far you have come. Reaching an end is reported as well.
Keyboard
| Key | Action |
|---|---|
| ↑ / ↓ / ← / → | Scroll the region (browser default once focusable). |
| PageUp / PageDown | Scroll by one page. |
| Home / End | Scroll to the start / end. |
<%# Markup for the scroll-area demo.
The consumer (this markup) puts aria-label on the viewport. On overflow the library
adds tabindex to the viewport and (only when it has an accessible name) role="region",
and exposes data-scroll (start/middle/end) and --stimeo--scroll-progress.
Drawing the scroll shadow is demo.css's job, reading data-scroll. %>
<div class="scroll-area" data-controller="stimeo--scroll-area"
data-stimeo--scroll-area-orientation-value="vertical">
<div class="scroll-area__viewport"
data-stimeo--scroll-area-target="viewport"
aria-label="<%= t("components.scroll_area.demo.label") %>">
<p><%= t("components.scroll_area.demo.intro") %></p>
<% 12.times do |i| %>
<p class="scroll-area__line"><%= t("components.scroll_area.demo.line", number: i + 1) %></p>
<% end %>
</div>
</div>
/* Presentation CSS for scroll-area. The library handles overflow detection, keyboard
reachability, and exposing data-scroll, so visuals like the scroll shadow are drawn
here by reading data-scroll. */
.scroll-area {
position: relative;
max-width: 420px;
border: 1px solid var(--border-strong);
border-radius: 8px;
}
/* The viewport is the element that actually scrolls; the library adds tabindex/role on overflow. */
.scroll-area__viewport {
max-height: 180px;
overflow-y: auto;
padding: 1rem;
}
/* Visible keyboard focus (tabindex=0 is added on overflow). */
.scroll-area__viewport:focus-visible {
outline: 2px solid var(--color-primary-hover);
outline-offset: -2px;
}
.scroll-area__viewport p {
margin: 0 0 0.75rem;
color: var(--color-text);
}
.scroll-area__line {
padding: 0.25rem 0;
border-bottom: 1px dashed var(--border-default);
}
/* Using data-scroll as the hook, show the scroll shadow only at the top/bottom edges. */
.scroll-area::before,
.scroll-area::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 24px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
.scroll-area::before {
top: 0;
background: linear-gradient(to bottom, rgba(15, 23, 42, 0.12), transparent);
}
.scroll-area::after {
bottom: 0;
background: linear-gradient(to top, rgba(15, 23, 42, 0.12), transparent);
}
/* If scrolled below the top (middle/end) show the top shadow; if above the bottom
show the bottom shadow. */
.scroll-area[data-scroll="middle"]::before,
.scroll-area[data-scroll="end"]::before {
opacity: 1;
}
.scroll-area[data-scroll="start"]::after,
.scroll-area[data-scroll="middle"]::after {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.scroll-area::before,
.scroll-area::after {
transition: 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-area"
Targets
| Name | Description | Attribute |
|---|---|---|
viewport
required
|
The natively scrolling region whose overflow and scroll position are tracked. | data-stimeo--scroll-area-target="viewport" |
Values
| Name | Description | Attribute |
|---|---|---|
orientation
|
The scroll axis to track, vertical, horizontal, or both (default vertical). |
data-stimeo--scroll-area-orientation-value |
Events
| Name | Description | Event |
|---|---|---|
reach
|
Dispatched once when scrolling reaches an edge; detail carries the edge (start/end). | stimeo--scroll-area:reach |
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-scroll |
Root element | "start" / "middle" / "end" scroll position. |
data-overflow |
Root element | "true" when the region is scrollable. |
--stimeo--scroll-progress |
Root element | 0–1 scroll progress. |