@scroll-area/svelte
v0.1.0
Published
Small, composable scroll primitives for Svelte 5: a headless controller plus overridable track/fade/nav/hint components.
Maintainers
Readme
@scroll-area/svelte
Small, composable scroll primitives for Svelte 5 (Runes). SSR-safe, RTL-aware, zero-cost when idle, and fully overridable via snippets.
Install
npm install @scroll-area/svelteRequires svelte@^5.0.0 as a peer dependency.
Quick start
<script lang="ts">
import { ScrollArea } from '@scroll-area/svelte';
</script>
<ScrollArea>
{#snippet children()}
{#each items as item}
<div class="w-64 shrink-0">{item.title}</div>
{/each}
{/snippet}
</ScrollArea>ScrollArea is a pre-wired shell: overlay nav buttons, edge fades, and a
scrollable track, all backed by a ScrollController. Every part of the UI is
overridable via snippets (navigation, prevButton, nextButton), and you
can drop down to the individual primitives (ScrollTrack, EdgeFade,
EdgeHover, ScrollHint, NavButton) to build your own layout.
The composable pattern
Create a ScrollController outside any component to share scroll state with
sibling UI (e.g. a header nav bar) without lifting it into a store:
<script lang="ts">
import { ScrollController, ScrollTrack, EdgeFade } from '@scroll-area/svelte';
const ctrl = new ScrollController({ itemsPerView: 5 });
</script>
<div class="flex justify-between">
<h2>Featured</h2>
<div>
<button disabled={!ctrl.canScrollPrev} onclick={() => ctrl.scrollPrev()}>←</button>
<button disabled={!ctrl.canScrollNext} onclick={() => ctrl.scrollNext()}>→</button>
</div>
</div>
<div class="relative">
<EdgeFade side="left" visible={ctrl.canScrollPrev} />
<EdgeFade side="right" visible={ctrl.canScrollNext} />
<ScrollTrack controller={ctrl} orientation="horizontal" class="gap-3 px-4">
{#each items as item}
<div class="w-[calc(100%/5.2)] shrink-0"><ItemCard {item} /></div>
{/each}
</ScrollTrack>
</div>ScrollController
new ScrollController(options?: ScrollControllerOptions)Options
| Option | Type | Default | Description |
| -------------- | ------------------- | -------------- | ------------------------------------ |
| orientation | ScrollOrientation | 'horizontal' | Axis or axes to scroll |
| step | number | — | Explicit px per step |
| itemsPerView | number | — | step = containerSize / itemsPerView |
| behavior | ScrollBehavior | 'smooth' | Forwarded to element.scrollTo() |
| rtl | boolean | false | Initial RTL; auto-updated from DOM |
Reactive state
| Property | Type | Description |
| ------------------ | --------- | --------------------------------------- |
| canScrollNext | boolean | Forward scroll possible |
| canScrollPrev | boolean | Backward scroll possible |
| isOverflowing | boolean | Content overflows the container |
| isHoverScrolling | boolean | A startScrolling() loop is running |
| scrollPosition | {x, y} | Each axis: { offset, max, progress } |
| rtl | boolean | Reactive; updated by MutationObserver |
Methods
| Method | Description |
| -------------------------- | ------------------------------------------------- |
| mount(el) | Attaches to a scroll container (called by ScrollTrack) |
| destroy() | Tears down all listeners/observers/loops; idempotent |
| scrollNext() / scrollPrev() | One step forward/backward; no-op at the boundary |
| scrollTo(target) | Scroll to a child index or HTMLElement |
| startScrolling(options) | Begin a continuous rAF scroll loop (idempotent) |
| stopScrolling() | Stop the loop; safe to call when nothing is running |
| watchScroll(callback) | Subscribe to scroll percentage; returns a cleanup fn |
startScrolling / stopScrolling
Used by hover-zone or long-press UI. The loop accelerates from minSpeed to
maxSpeed over ~60 frames. isHoverScrolling is true while running.
<EdgeHover side="prev" enabled={ctrl.canScrollPrev} controller={ctrl}>
<EdgeFade side="left" visible={ctrl.canScrollPrev}>
<ScrollHint side="left" visible={!ctrl.isHoverScrolling} />
</EdgeFade>
</EdgeHover>Reactive RTL
The controller watches the dir attribute on the scroll element and every
ancestor via MutationObserver. Flipping dir anywhere in that chain
instantly updates ctrl.rtl, boundary detection, and navigation — no reload
required.
Vertical orientation — important
ScrollArea / ScrollTrack fill their parent (h-full internally). The
parent must have an explicit height, or the container collapses to 0.
<!-- ✓ Correct: parent has an explicit height -->
<div class="h-64">
<ScrollArea orientation="vertical">...</ScrollArea>
</div>
<!-- ✗ Broken: no height constraint, ScrollTrack collapses -->
<ScrollArea orientation="vertical">...</ScrollArea>Exports
| Export | Role |
| -------------------- | --------------------------------------------------------------------------------- |
| ScrollController | All scroll state and logic. Create outside any component to share state. |
| ScrollTrack | The scrollable surface. Binds the controller to a DOM element. |
| EdgeFade | Gradient edge indicator. Accepts children for nav buttons and scroll hints. |
| EdgeHover | Hover-to-scroll wrapper; calls startScrolling/stopScrolling on pointer enter/leave. |
| ScrollHint | Animated hint nudging toward hidden content. |
| ScrollArea | Pre-wired shell with overlay navigation, all UI overridable via snippets. |
| NavButton | Default orientation-aware nav button used internally by ScrollArea. |
| Types | ScrollAreaProps, ScrollControllerOptions, ScrollScrollingOptions, ScrollPosition, AxisScrollPosition, ScrollTarget, NavButtonProps, EdgeFadeProps, EdgeHoverProps, ScrollHintProps, WatchScrollCleanup |
| Enums | ScrollBehavior, ScrollDirection, ScrollOrientation, EdgeSide |
| Constants | DEFAULT_STEP_RATIO, DEFAULT_SCROLL_BEHAVIOR, SCROLL_BOUNDARY_TOLERANCE |
| Low-level utils | getHorizontalPosition, getVerticalPosition, getScrollPosition, canScrollHorizontalNext, canScrollHorizontalPrev, canScrollVerticalNext, canScrollVerticalPrev, isOverflowingHorizontal, isOverflowingVertical, scrollHorizontalByDelta, scrollVerticalByDelta, scrollToTarget, resolveStep, detectRtlFromDom, debounce |
The low-level utils, constants, and types are exported so you can build a custom scroll UI (or a different feature entirely) on top of the same primitives instead of duplicating them.
Icons
Default nav/hint icons come from lucide-svelte,
a direct dependency of this package. Override any icon by passing your own
content via the relevant children snippet — see NavButtonProps,
ScrollHintProps.
Example
A full runnable demo covering every export lives in examples/svelte at the
repo root — see its own README.md for how to run it.
License
MIT
