Scroll
Description
A primitive component for building advanced scrolling experiences. It provides extra features compared to normal scroll containers, as well as performance optimizations when used inside or around a Sheet component.
We recommend using the Scroll component for all scrolling across your app.
Anatomy
TypeScript
import { Scroll } from "@silk-hq/sheet";
export default () => (
<Scroll.Root>
<Scroll.View>
<Scroll.Content>...</Scroll.Content>
</Scroll.View>
<Scroll.Trigger />
</Scroll.Root>
);Sub-components
<Scroll.Root>
| Characteristic | Details |
|---|---|
| Presence | Required |
| Composition | Contain all other Scroll sub-components |
| Underlying element | |
Description
The Root sub-component wraps all other Scroll sub-components of the same Scroll instance, as it contains logic shared among all.
asChild
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether the sub-component underlying element is the default one or the one passed as child of the sub-component.
Values description
| Value | Description |
|---|---|
| The underlying element rendered is the child. |
| The underlying element rendered is the default one. |
Notes
- If the child is a React component rendering an element:
- it must accept props and spread all received props onto the rendered element;
- in React < 19, it must use
React.forwardRef()and pass the received ref to the rendered element.
- See Composition for more information.
componentId
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the id of the Scroll component instance. This id can then be passed to other Scroll sub-components forComponent prop to associate them with it.
componentRef
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | React.RefObject<ScrollRef> where ScrollRef is:{
getProgress: () => number;
getDistance: () => number;
getAvailableDistance: () => number;
scrollTo: (options: ScrollToOptions) => void;
scrollBy: (options: ScrollByOptions) => void;
} where ScrollToOptions = ScrollByOptions is: {
progress?: number;
distance?: number;
animationSettings?: { skip: "default" | "auto" | boolean };
} |
| Default | |
Description
Associates a React.RefObject to <Scroll.Root> which can then be used to control the Scroll component imperatively by calling the methods stored in it.
Methods description
| Value | Description |
|---|---|
| Returns the scroll progress from 0 to 1. When <Scroll.Content> start edge is aligned with <Scroll.View> start edge, scroll progress is 0. When they are aligned on their end edge, scroll progress is 1. |
| Returns the distance in pixels traveled by <Scroll.Content> from its start position. |
| Returns the distance in pixels that <Scroll.Content> can travel in total, from its start position to its end position. |
| Make <Scroll.Content> travel so it ends up at the defined progress or distance. |
If the animationSettings skip key value computes to false, then animation occurs; if it computes to true the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, and computes to false otherwise. |
| scrollBy | Make the <Scroll.Content> travel by the defined progress or distance.
If the animationSettings skip key value computes to false, then animation occurs; if it computes to true the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, and computes to false otherwise. |
<Scroll.Trigger>
| Characteristic | Details |
|---|---|
| Presence | Required |
| Composition | Descendant of <Scroll.Root> |
| Underlying element | |
Description
A Trigger sub-component that allows to run specific actions related to the Scroll instance as a result of a press event.
asChild
See asChild on <Scroll.Root>.
forComponent
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | The ScrollId of the closest <Scroll.Root> ancestor. |
Description
Associates this sub-component with the desired Scroll instance.
action
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the action that will execute when the Trigger is pressed.
Values description
| Value | Description |
|---|---|
| Make <Scroll.Content> travel so it ends up at the defined progress or distance. If the animationSettings skip key value computes to false, then animation occurs, if it computes to false the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, it computes to false otherwise. |
| Make <Scroll.Content> travel by the defined progress or distance. If the animationSettings skip key value computes to false, then animation occurs, if it computes to false the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, and computes to false otherwise. |
onPress
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
An event handler that runs when the Trigger is pressed (equivalent to clicked).
The underlying custom event has a default behavior that can be changed either by calling its changeDefault method with an option object as parameter, or by directly passing the option object to the prop.
Values description
| Value | Description |
|---|---|
| The underlying element will be focused on press in all browsers (by default Safari doesn’t do it, causing issues with focus management). This is the recommended setting. |
| Inverse of true. |
| The Trigger action will be run. |
| Inverse of true. |
Example
TypeScript
<Scroll.Trigger onPress={{ forceFocus: false }}>...</Scroll.Trigger>;TypeScript
<Scroll.Trigger onPress={(event) => event.changeDefault({ forceFocus: false })}>
...
</Scroll.Trigger>;<Scroll.View>
| Characteristic | Details |
|---|---|
| Presence | Required |
| Composition | Descendant of <Scroll.Root> |
| Underlying element | |
Description
The View sub-component is the area inside of which the <Scroll.Content> can travel.
Notes
- Elements put directly inside of this sub-component will not move along the content as scroll occurs.
- If you are using this component inside of nested CSS grid or flex containers, you may need to add
min-width: 0px;and/ormin-height: 0px;on these containers’ children to prevent<Scroll.View>being sized based on<Scroll.Content>size on the scroll axis, thus causing visible overflow instead of a scrollable overflow.
asChild
See asChild on <Scroll.Root>.
forComponent
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | The ScrollId of the closest <Scroll.Root> ancestor. |
Description
Associates this sub-component with the desired Scroll instance.
axis
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the axis on which <Scroll.Content> can travel.
Notes
In macOS Safari, when pageScroll is set to true and nativePageScrollReplacement computes to false, it is possible to cause scroll overshoot on the non-scrolling axis with a scroll gesture. The only way to prevent this behavior is to set nativePageScrollReplacement to true.
pageScroll
| Characteristic | Details |
|---|---|
| Presence | Required if nativePageScrollReplacement is set to true or "auto". |
| Type | |
| Default | |
Description
Defines whether this Scroll component is considered as a page.
When set to true, this Scroll can be used to control page scrolling (no matter whether it is native or replaced). Therefore, you can use a <Scroll.Trigger>, or imperative methods to get scroll informations or cause scrolling. You can therefore use the exact same API to control any scroll container and page scrolling.
Values description
| Value | Description |
|---|---|
| The underlying element acts a scroll container. |
| Page scrolling can be controlled through this Scroll component instance. - If nativePageScrollReplacement computes to false, the <Scroll.View> underlying element does not act as a scroll container, instead it acts as a simple element. - If nativePageScrollReplacement computes to true, the <Scroll.View> underlying element acts as a scroll container replacing native page scrolling. |
nativePageScrollReplacement
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
| Requirements | - pageScroll must be set to true to use this prop with a value of true or "auto". - With SSG or SSR, when set to true or "auto", suppressHydrationWarning must be set on <html>. |
Description
Defines whether native page scrolling (a.k.a. “body scrolling”) is being replaced by the Scroll component scroll container.
Values description
| Value | Description |
|---|---|
| Native page scrolling is replaced by the Scroll component scroll container. |
| Inverse of true. |
| Computes to false in mobile browsers (i.e. Android, iOS iPadOS), except in standalone mode; computes to true everywhere else. |
Notes
- When computing to
true:- Benefits:
- Prevents elements viewport shift when the on-screen keyboard appears.
- Prevents native pull-down to refresh on iOS.
- Prevents scroll overshoot on the non-scrolling axis in macOS Safari.
- Improves animation performance when animating
<Scroll.View>or ancestors. - Swiping over elements with
position: fixed;doesn’t cause page scrolling.
- Limitations (use
"auto"to work around them in mobile browsers):- Native scroll into view for text fragments (e.g.
#:~:text=example) does not work. - Native scroll into view for anchors (e.g.
#anchor-id) does not work. - Native scroll to top by tapping the status bar does not work on iOS.
- Native pull-down to refresh does not work on iOS.
- Mobile browser UIs don't collapse as the user scrolls.
- Native scroll into view for text fragments (e.g.
- Benefits:
- The
useNativePageScrollReplacedhook returns abooleanindicating whether native page scroll is currently replaced.
safeArea
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the area of the viewport that is considered safe for <Scroll.Content> to travel within. If <Scroll.View> overflows this area, then the available scroll distance is increased so that <Scroll.Content> can travel as much as required for it to be fully accessible to the user.
For example, if <Scroll.View> fills the entire layout viewport, then when the on-screen keyboard appears and the visual viewport shrinks, the bottom part of the <Scroll.View> underlying HTML will be hidden behind the on-screen keyboard, and the <Scroll.Content> will not be fully accessible to the user. By setting this prop to "visual-viewport", the scrolling distance gets expanded so <Scroll.Content> can travel as much as needed to be entirely visible above the on-screen keyboard.
Values description
| Value | Description |
|---|---|
| No safe area is defined. |
| The safe area is defined by the bounds of the layout viewport (i.e. the browser window excluding the browser interface). |
| The safe area is defined by the bounds of the visual viewport (i.e. the browser window excluding the browser interface and the on-screen keyboard). |
scrollGestureTrap
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether scroll gestures performed in a direction where further scrolling cannot occur (because the edge has been reached, or because there is no overflow) should be trapped inside of <Scroll.View>, or propagate to the page, ancestor scroll containers, or Sheets, causing swipe.
Notes
- Contrary to the
overscroll-behaviorCSS property in most browsers, this mechanism works even when there is no scroll overflow. - When trapping is enabled, native overscroll actions are prevented (except when using native page scrolling in Safari). For example, if trapping is enabled on
yStart, pull to refresh in mobile browsers is prevented; if it is enabled onxStart, swipe to go back in history in desktop browsers is prevented. - If
scrollGestureOvershootis set tofalse, thenscrollGestureTrapalways computes totrue. - Due to a Safari bug, trapping is always enabled when swiping over a vertical Scroll component wrapped in an horizontally swipeable Sheet itself wrapped in a vertically swipeable Sheet component. We hope to see that bug resolved quickly.
scrollGestureOvershoot
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the visual behavior of <Scroll.Content> when the user performs a scroll gesture in a direction where scrolling cannot occur (because the edge has been reached).
Values description
| Value | Description |
|---|---|
| Overshooting occurs. |
| Inverse of true. |
Notes
- Only iOS/iPadOS browsers and Safari and Firefox on macOS support overshooting.
- If
scrollGestureOvershootis set tofalse, thenscrollGestureTrapalways computes totrue.
scrollGesture
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether scrolling occurs as a result of user scroll gestures (mousewheel events, touchmove events, direction keys, start/end keys, dragging the scrollbar, etc.).
Values description
| Value | Description |
|---|---|
| Scrolling occurs as a result of user scroll gestures if <Scroll.Content> overflows <Scroll.View>. |
| Scrolling does not occur as a result of user scroll gestures. |
Notes
In iOS browsers (all using WebKit under the hood), when pageScroll is set to true and nativePageScrollReplacement computes to false, Apple is intentionnally preventing scrollGesture={false} from working reliably. You can use nativePageScrollReplacement={true} to work around this limitation.
onScroll
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
An event handler that runs asynchronously on every frame when scrolling occurs, whether it is caused by a scroll gesture or programmatically.
Parameters description
| Value | Description |
|---|---|
| The scroll progress from 0 to 1. When <Scroll.Content> start edge is aligned with <Scroll.View> start edge, scroll progress is 0. When they are aligned on their end edge, scroll progress is 1. |
| The distance in pixels traveled by <Scroll.Content> from its start position. |
| The distance in pixels that <Scroll.Content> can travel in total, from its start position to its end position. |
| The underlying native scroll event. |
onScrollStart
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | { dismissKeyboard: boolean } | ((customEvent: ScrollStartCustomEvent) => void where ScrollStartCustomEvent is {
changeDefault: (changedBehavior: { dismissKeyboard: boolean }) => void;
dismissKeyboard: boolean;
nativeEvent: null;
} |
| Default | |
Description
An event handler that runs when scrolling starts, whether it is initiated by a scroll gesture or programmatically.
The underlying custom event has a default behavior that can be changed either by calling its changeDefault method with an option object as parameter, or by directly passing the option object to the prop.
Values description
| Value | Description |
|---|---|
| Causes the on-screen keyboard to be dismissed if it is presented when the event is fired. |
| Inverse of { dismissKeyboard: true }. |
Example
TypeScript
<Scroll.View onFocusInside={{ dismissKeyboard: true }}>...</Scroll.View>;TypeScript
<Sheet.View
onFocusInside={(event) => event.changeDefault({ dismissKeyboard: true })}
>
...
</Sheet.View>;onScrollEnd
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
An event handler that runs when scrolling ends, whether it was initiated by a scroll gesture or programmatically.
nativeFocusScrollPrevention
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether the native scroll into view mechanism should be prevented or not when a <Scroll.View> descendant element receives focus.
When an element receives focus on mobile, the browser shifts the viewport up or down to try and keep it in view. Unfortunately, this native mechanism doesn’t work well in most situations, so we let you prevent the default behavior to properly deal with the position of the focused element. We recommend using the onFocusInside to do so.
Notes
- The prevention doesn’t work when the inputs are inside of an
<iframe>element. - When the user clicks on text present in a text input, the caret will always be put back at its previous position when set to
true. - In iOS Safari, the prevention may not be effective with password inputs whose
autoCompletehtml attribute is set to anything else than"current-password". Therefore, we strongly recommend always using this value. Safari won’t suggest a new password directly, but the user can still get a suggested password by tapping the “Password” button in the suggestion bar and asking the password manager to provide a new password. - Due to a Chromium Android bug, the prevention won’t work when the
scrollIntoViewoption ononFocusInsideis set tofalse. Please consider voting and commenting on the Chromium issue so it gets prioritized. - Due to a Chromium Android bug, the prevention may not be effective when the focus is moved with the on-screen keyboard “next input” button. Please consider voting and commenting on the Chromium issue so it gets prioritized.
- Due to a Chrome Android bug, for inputs causing the suggestion bar of the keyboard to be shown, a distance of at least 48px between the last text input and the bottom of the viewport is required to avoid a layout shift.
onFocusInside
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | | { scrollIntoView: boolean }
| ((customEvent: ScrollViewFocusInsideCustomEvent) => void) where ScrollViewFocusInsideCustomEvent is {
changeDefault: (changedBehavior: { scrollIntoView: boolean }) => void;
scrollIntoView: boolean;
nativeEvent: Event;
} |
| Default | |
Description
An event handler that runs when a <Scroll.View> descendant element receives focus.
The underlying custom event has a default behavior that can be changed either by calling its changeDefault method with an option object as parameter, or by directly passing the option object to the prop.
Values description
| Value | Description |
|---|---|
| The element receiving focus is scrolled into view so it is fully visible. |
| Inverse of { scrollIntoView: true }. |
Notes
- The
scrollIntoViewoption is a reliable alternative to the native scroll into view on focus mechanism that can disabled withnativeFocusScrollPrevention. - The
scrollIntoViewoption takes thesafeAreainto account. - Due to a Chrome Android bug, the
nativeFocusScrollPreventionmechanism won’t work when using{ scrollIntoView: false }. Please consider voting and commenting on the Chromium issue so it gets prioritized.
Example
TypeScript
<Scroll.View onFocusInside={{ scrollIntoView: false }}>...</Scroll.View>;TypeScript
<Sheet.View
onFocusInside={(event) => event.changeDefault({ scrollIntoView: false })}
>
...
</Sheet.View>;scrollAnimationSettings
| Characteristic | Details |
|---|---|
| Presence | no |
| Type | |
| Default | |
Description
Defines the animation settings for programmatic scrolling (i.e. scroll caused by <Scroll.Trigger> or an imperative call).
Values description
| Value | Description |
|---|---|
| Programmatic scrolling is animated only if the user does not prefer “reduced motion”. |
| Programmatic scrolling is animated. |
| Inverse of { skip: false }. |
scrollAnchoring
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether the scroll position should be adjusted to prevent sudden changes when a layout shift occurs inside of <Scroll.Content>.
Notes
- Uses the
overflow-anchorCSS property under the hood. - Not yet supported in Safari.
scrollSnapType
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines the value for the scroll-snap-type CSS property.
scrollPadding
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | "auto" | string (CSS length) |
| Default | |
Description
Defines the value for the scroll-padding CSS property.
scrollTimelineName
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | "none" | string (prefixed with "--") |
| Default | |
Description
Defines the value for the scroll-timeline-name CSS property.
nativeScrollbar
| Characteristic | Details |
|---|---|
| Presence | Optional |
| Type | |
| Default | |
Description
Defines whether the native scrollbar should be displayed or not.
<Scroll.Content>
| Characteristic | Details |
|---|---|
| Presence | Required |
| Composition | Descendant of <Scroll.View> |
| Underlying element | |
Description
The Content sub-component represents the content that moves as scroll occurs.
asChild
See asChild on <Scroll.Root>.
Imperative handling
You can manipulate imperatively the Scroll component by passing a React ref to the <Scroll.Root> sub-component componentRef prop and then calling the methods stored on it.
getProgress
| Characteristic | Details |
|---|---|
| Type | |
Description
Returns the scroll progress from 0 to 1. When <Scroll.Content> start edge is aligned with <Scroll.View> start edge, scroll progress is 0. When they are aligned on their end edge, scroll progress is 1.
getDistance
| Characteristic | Details |
|---|---|
| Type | |
Description
Returns the distance in pixels traveled by <Scroll.Content> from its start position.
getAvailableDistance
| Characteristic | Details |
|---|---|
| Type | |
Description
Returns the distance in pixels that <Scroll.Content> can travel in total, from its start position to its end position.
scrollTo
| Characteristic | Details |
|---|---|
| Type | scrollTo: (options: ScrollToOptions) => void where ScrollToOptions is {
progress?: number;
distance?: number;
animationSettings?: { skip: "default" | "auto" | boolean };
} |
Description
Make <Scroll.Content> travel so it ends up at the defined progress or distance.
If the animationSettings skip key value computes to false, then animation occurs; if it computes to true the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, and computes to false otherwise.
scrollBy
| Characteristic | Details |
|---|---|
| Type | scrollTo: (options: ScrollByOptions) => void where ScrollByOptions is {
progress?: number;
distance?: number;
animationSettings?: { skip: "default" | "auto" | boolean };
} |
Description
Make <Scroll.Content> travel by the defined progress or distance.
If the animationSettings skip key value computes to false, then animation occurs; if it computes to true the animation is skipped. "default" computes to the value provided in the scrollAnimationSettings prop on <Scroll.View>. "auto" computes to true when the user has prefers-reduced-motion enabled, and computes to false otherwise.