Skip to content
Destyler UI Destyler UI Destyler UI

Scroll Area

A scroll area component with custom scrollbars that augments native scroll functionality for custom, cross-browser styling.

Tags
v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
v1.2.0-beta.35
v1.2.0-beta.34
v1.2.0-beta.33
v1.2.0-beta.32
v1.2.0-beta.31
v1.2.0-beta.30
v1.2.0-beta.29
v1.2.0-beta.28
v1.2.0-beta.27
v1.2.0-beta.26
v1.2.0-beta.25
v1.2.0-beta.24
v1.2.0-beta.23
v1.2.0-beta.22
v1.2.0-beta.21
v1.2.0-beta.20
v1.2.0-beta.19
v1.2.0-beta.18
v1.2.0-beta.17
v1.2.0-beta.16
v1.2.0-beta.15
v1.2.0-beta.14
v1.2.0-beta.13
v1.2.0-beta.12
v1.2.0-beta.11
v1.2.0-beta.10
v1.2.0-beta.9
v1.2.0-beta.8
v1.2.0-beta.7
v1.2.0-beta.6
v1.2.0-beta.5
v1.2.0-beta.4
v1.2.0-beta.3
v1.2.0-beta.2
v1.2.0-beta.1
Tags
v1.2.0-beta.50
v1.2.0-beta.49
v1.2.0-beta.48
v1.2.0-beta.47
v1.2.0-beta.46
v1.2.0-beta.45
v1.2.0-beta.44
v1.2.0-beta.43
v1.2.0-beta.42
v1.2.0-beta.41
v1.2.0-beta.40
v1.2.0-beta.39
v1.2.0-beta.38
v1.2.0-beta.37
v1.2.0-beta.36
v1.2.0-beta.35
v1.2.0-beta.34
v1.2.0-beta.33
v1.2.0-beta.32
v1.2.0-beta.31
v1.2.0-beta.30
v1.2.0-beta.29
v1.2.0-beta.28
v1.2.0-beta.27
v1.2.0-beta.26
v1.2.0-beta.25
v1.2.0-beta.24
v1.2.0-beta.23
v1.2.0-beta.22
v1.2.0-beta.21
v1.2.0-beta.20
v1.2.0-beta.19
v1.2.0-beta.18
v1.2.0-beta.17
v1.2.0-beta.16
v1.2.0-beta.15
v1.2.0-beta.14
v1.2.0-beta.13
v1.2.0-beta.12
v1.2.0-beta.11
v1.2.0-beta.10
v1.2.0-beta.9
v1.2.0-beta.8
v1.2.0-beta.7
v1.2.0-beta.6
v1.2.0-beta.5
v1.2.0-beta.4
v1.2.0-beta.3
v1.2.0-beta.2
v1.2.0-beta.1

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/scroll-area @destyler/vue
Terminal window
      
        
npm install @destyler/scroll-area @destyler/react
Terminal window
      
        
npm install @destyler/scroll-area @destyler/svelte
Terminal window
      
        
npm install @destyler/scroll-area @destyler/solid

Anatomy

Import all parts and piece them together.

<script setup lang="ts">
import * as scrollArea from '@destyler/scroll-area'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const [state, send] = useMachine(scrollArea.machine({ id: useId() }))
const api = computed(() =>
scrollArea.connect(state.value, send, normalizeProps),
)
</script>
<template>
<div v-bind="api.getRootProps()">
<div v-bind="api.getViewportProps()">
<div v-bind="api.getContentProps()">
<!-- Your content here -->
</div>
</div>
<div v-bind="api.getScrollbarProps({ orientation: 'vertical' })">
<div v-bind="api.getThumbProps({ orientation: 'vertical' })" />
</div>
<div v-bind="api.getScrollbarProps({ orientation: 'horizontal' })">
<div v-bind="api.getThumbProps({ orientation: 'horizontal' })" />
</div>
<div v-bind="api.getCornerProps()" />
</div>
</template>
import * as scrollArea from '@destyler/scroll-area'
import { normalizeProps, useMachine } from '@destyler/react'
import { useId } from 'react'
export default function ScrollArea() {
const [state, send] = useMachine(scrollArea.machine({ id: useId() }))
const api = scrollArea.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<div {...api.getViewportProps()}>
<div {...api.getContentProps()}>
{/* Your content here */}
</div>
</div>
<div {...api.getScrollbarProps({ orientation: 'vertical' })}>
<div {...api.getThumbProps({ orientation: 'vertical' })} />
</div>
<div {...api.getScrollbarProps({ orientation: 'horizontal' })}>
<div {...api.getThumbProps({ orientation: 'horizontal' })} />
</div>
<div {...api.getCornerProps()} />
</div>
)
}
<script lang="ts">
import * as scrollArea from '@destyler/scroll-area'
import { normalizeProps, useMachine } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(scrollArea.machine({ id }))
const api = $derived(scrollArea.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<div {...api.getViewportProps()}>
<div {...api.getContentProps()}>
<!-- Your content here -->
</div>
</div>
<div {...api.getScrollbarProps({ orientation: 'vertical' })}>
<div {...api.getThumbProps({ orientation: 'vertical' })}></div>
</div>
<div {...api.getScrollbarProps({ orientation: 'horizontal' })}>
<div {...api.getThumbProps({ orientation: 'horizontal' })}></div>
</div>
<div {...api.getCornerProps()}></div>
</div>
import * as scrollArea from '@destyler/scroll-area'
import { normalizeProps, useMachine } from '@destyler/solid'
import { createMemo, createUniqueId } from 'solid-js'
export default function ScrollArea() {
const [state, send] = useMachine(scrollArea.machine({ id: createUniqueId() }))
const api = createMemo(() => scrollArea.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<div {...api().getViewportProps()}>
<div {...api().getContentProps()}>
{/* Your content here */}
</div>
</div>
<div {...api().getScrollbarProps({ orientation: 'vertical' })}>
<div {...api().getThumbProps({ orientation: 'vertical' })} />
</div>
<div {...api().getScrollbarProps({ orientation: 'horizontal' })}>
<div {...api().getThumbProps({ orientation: 'horizontal' })} />
</div>
<div {...api().getCornerProps()} />
</div>
)
}

Scrollbar visibility

You can configure when the scrollbars are visible using the type property. The available options are:

  • hover (default): Scrollbars are visible when hovering over the scroll area or while scrolling
  • scroll: Scrollbars are visible only while scrolling
  • always: Scrollbars are always visible when content overflows
  • auto: Scrollbars are visible when content overflows
const [state, send] = useMachine(
scrollArea.machine({
type: 'always',
}),
)

Scrollbar hide delay

When using scroll or hover visibility type, you can configure how long the scrollbars remain visible after the user stops interacting:

const [state, send] = useMachine(
scrollArea.machine({
type: 'scroll',
scrollHideDelay: 1000, // Hide after 1 second
}),
)

Listening for scroll changes

When the scroll position changes, the onScroll callback is invoked.

const [state, send] = useMachine(
scrollArea.machine({
onScroll(details) {
// details => { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth }
console.log('Scroll position:', details.scrollTop, details.scrollLeft)
},
}),
)

Virtual scrolling

For large lists, you can enable virtual scrolling to only render visible items. This significantly improves performance when dealing with thousands of items.

const [state, send] = useMachine(
scrollArea.machine({
virtual: {
count: 10000, // Total number of items
itemSize: 50, // Height of each item (in pixels)
overscan: 5, // Extra items to render outside viewport
orientation: 'vertical', // 'vertical' or 'horizontal'
},
}),
)
// Then render virtual items
const virtualItems = api.getVirtualItems()

Rendering virtual items

When using virtual scrolling, render items using the getVirtualItems() method:

<template>
<div v-bind="api.getRootProps()">
<div v-bind="api.getViewportProps()">
<div v-bind="api.getContentProps()">
<div
v-for="item in api.getVirtualItems()"
:key="item.index"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}"
>
Item {{ item.index }}
</div>
</div>
</div>
<!-- scrollbars... -->
</div>
</template>
<div {...api.getRootProps()}>
<div {...api.getViewportProps()}>
<div {...api.getContentProps()}>
{api.getVirtualItems().map(item => (
<div
key={item.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
Item {item.index}
</div>
))}
</div>
</div>
{/* scrollbars... */}
</div>
<div {...api.getRootProps()}>
<div {...api.getViewportProps()}>
<div {...api.getContentProps()}>
{#each api.getVirtualItems() as item}
<div
style="position: absolute; top: 0; left: 0; width: 100%; height: {item.size}px; transform: translateY({item.start}px);"
>
Item {item.index}
</div>
{/each}
</div>
</div>
<!-- scrollbars... -->
</div>
<div {...api().getRootProps()}>
<div {...api().getViewportProps()}>
<div {...api().getContentProps()}>
<For each={api().getVirtualItems()}>
{item => (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
Item {item.index}
</div>
)}
</For>
</div>
</div>
{/* scrollbars... */}
</div>

Scroll to index

You can programmatically scroll to a specific item index:

// Scroll to item at index 500
api.scrollToIndex(500)
// Scroll with alignment option
api.scrollToIndex(500, { align: 'center' }) // 'start' | 'center' | 'end' | 'auto'

Dynamic item sizes

For items with varying heights, you can provide a function instead of a fixed size:

const [state, send] = useMachine(
scrollArea.machine({
virtual: {
count: 10000,
itemSize: (index) => {
// Return different sizes based on item type
return index % 10 === 0 ? 100 : 50
},
},
}),
)

Programmatic scrolling

You can scroll to a specific position programmatically:

// Scroll to absolute position
api.scrollTo({ top: 500 })
// Scroll to position with smooth behavior
api.scrollTo({ top: 500, behavior: 'smooth' })
// Scroll horizontally
api.scrollTo({ left: 200 })

Styling Guide

Each scroll area part has a data-part attribute added to them to select and style them in the DOM.

Scrollbar visibility state

When the scrollbar is visible or hidden, the data-state attribute is set:

[data-scope="scroll-area"][data-part="scrollbar"][data-state="visible"] {
/* styles for visible scrollbar */
}
[data-scope="scroll-area"][data-part="scrollbar"][data-state="hidden"] {
/* styles for hidden scrollbar */
}

Scrollbar orientation

Style scrollbars based on their orientation:

[data-scope="scroll-area"][data-part="scrollbar"][data-orientation="vertical"] {
width: 10px;
}
[data-scope="scroll-area"][data-part="scrollbar"][data-orientation="horizontal"] {
height: 10px;
}

Thumb dragging state

When the user is dragging the scrollbar thumb:

[data-scope="scroll-area"][data-part="thumb"][data-dragging] {
/* styles while dragging */
background-color: rgba(0, 0, 0, 0.8);
}

Hiding native scrollbars

The component automatically hides native scrollbars, but you can add additional styles for webkit browsers:

[data-scope="scroll-area"][data-part="viewport"]::-webkit-scrollbar {
display: none;
}

Methods and Properties

Machine Context

The scroll area machine exposes the following context properties:

ids
Partial<{ root: string; viewport: string; content: string; scrollbarX: string; scrollbarY: string; thumbX: string; thumbY: string; corner: string; }>
The ids of the elements in the scroll area. Useful for composition.
type(default: 'hover')
ScrollType
Describes the nature of scrollbar visibility - `auto`: Scrollbars are visible when content is overflowing - `always`: Scrollbars are always visible - `scroll`: Scrollbars are visible when the user is scrolling - `hover`: Scrollbars are visible when the user is hovering over the scroll area
scrollHideDelay(default: 600)
number
If type is set to either `scroll` or `hover`, this prop determines the length of time, in milliseconds, before the scrollbars are hidden after the user stops interacting with scrollbars.
onScroll
(details: ScrollChangeDetails) => void
Callback when scroll position changes
virtual
VirtualScrollOptions
Virtual scroll configuration. When provided, enables virtual scrolling.
dir(default: "ltr")
"ltr" | "rtl"
The document's text/writing direction.
id
string
The unique identifier of the machine.
getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

Machine API

The scroll area api exposes the following methods:

scrollTop
number
The current scroll position
hasOverflowX
boolean
Whether there is overflow in each direction
scrollbarXVisible
boolean
Whether scrollbars are currently visible
scrollTo
(options: ScrollToOptions) => void
Scroll to a specific position
scrollToIndex
(index: number, options?: { align?: "auto" | "start" | "center" | "end"; }) => void
Scroll to a specific item index (for virtual scrolling)
measureItem
(index: number, size: number) => void
Measure a specific item's size (for dynamic virtual scrolling)
getVirtualItems
() => VirtualItem[]
Get virtual scroll state (when virtual scrolling is enabled)
getTotalSize
() => number
Get the total size of all virtual items
getVisibleRange
() => VirtualRange
Get the visible range of items
getRootProps
() => T["element"]
Props for the root element
getViewportProps
() => T["element"]
Props for the viewport element
getContentProps
() => T["element"]
Props for the content element (wrapper for virtual items)
getScrollbarProps
(options: ScrollbarProps) => T["element"]
Props for a scrollbar
getThumbProps
(options: ScrollbarProps) => T["element"]
Props for the scrollbar thumb
getCornerProps
() => T["element"]
Props for the corner element

Data Attributes

Root

attribute
description
data-scope
scroll-area
data-part
root
data-orientation
The orientation of the scroll-area

Viewport

attribute
description
data-scope
scroll-area
data-part
viewport
data-orientation
The orientation of the viewport

Content

attribute
description
data-scope
scroll-area
data-part
content
data-orientation
The orientation of the content

Scrollbar

attribute
description
data-scope
scroll-area
data-part
scrollbar
data-orientation
The orientation of the scrollbar
data-state
"visible" | "hidden"

Thumb

attribute
description
data-scope
scroll-area
data-part
thumb
data-orientation
The orientation of the thumb
data-state
"visible" | "hidden"
data-dragging
Present when in the dragging state

Corner

attribute
description
data-scope
scroll-area
data-part
corner
data-state
"visible" | "hidden"

Accessibility

The scroll area component uses semantic ARIA attributes:

  • The viewport has tabIndex={0} for keyboard accessibility
  • Scrollbars have proper role="scrollbar" with aria-controls, aria-orientation, aria-valuenow, aria-valuemin, and aria-valuemax attributes