A Vue 3 component for a scroll area with DOM-level custom scrollbars.
The idea and original implementation come from @07akioni.
Original repo: react-scroll-area-demo
This project ports the idea to Vue 3.
pnpm i vue-scroll-area<template>
<ScrollArea
:style="{ height: '240px', width: '480px' }"
:vertical-scrollbar-min-height="20"
:horizontal-scrollbar-min-width="20"
>
<div style="width: 960px; min-height: 640px;">
Your scrollable content goes here.
</div>
</ScrollArea>
</template>
<script setup lang="ts">
import { ScrollArea } from 'vue-scroll-area'
</script>Use ScrollAreaResizeableContent when the content size can change after mount.
<template>
<ScrollArea :style="{ height: '240px', width: '480px' }">
<ScrollAreaResizeableContent>
<div style="width: 960px;">
Resizable content
</div>
</ScrollAreaResizeableContent>
</ScrollArea>
</template>
<script setup lang="ts">
import { ScrollArea, ScrollAreaResizeableContent } from 'vue-scroll-area'
</script>For a native textarea, render a custom container and set gutters-position="absolute" on
ScrollArea. The custom container owns the textarea element and places ScrollAreaGutters
inside the same positioned wrapper.
<!-- TextareaScroll.vue -->
<script setup lang="ts">
import { h } from 'vue'
import type { ScrollAreaContainerProps, ScrollAreaContainerRender } from 'vue-scroll-area'
import { ScrollArea } from 'vue-scroll-area'
import TextareaContainer from './TextareaContainer.vue'
const textareaContainerRenderer: ScrollAreaContainerRender = (props: ScrollAreaContainerProps) => {
return h(TextareaContainer, props)
}
</script>
<template>
<ScrollArea :container-render="textareaContainerRenderer" gutters-position="absolute" />
</template><!-- TextareaContainer.vue -->
<template>
<div class="textarea-shell">
<ScrollAreaGutters />
<textarea
:class="props.scrollAreaClass"
:ref="(el: any) => { props.containerDomRef.value = el }"
v-model="value"
wrap="off"
@input="props.onContentResize"
@scroll="props.onContainerScroll"
/>
<div ref="mirrorRef" class="textarea-mirror">
{{ value }}
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import type { ScrollAreaContainerProps } from 'vue-scroll-area'
import { ScrollAreaGutters } from 'vue-scroll-area'
const props = defineProps<ScrollAreaContainerProps>()
const value = ref(`Hello world
This textarea uses custom scrollbars.
This is a long line that can trigger horizontal scrolling when wrap is off.`)
const mirrorRef = ref<HTMLDivElement | null>(null)
let resizeObserver: ResizeObserver | null = null
onMounted(() => {
resizeObserver = new ResizeObserver(() => {
props.onContentResize()
props.onContainerResize()
})
if (props.containerDomRef.value) {
resizeObserver.observe(props.containerDomRef.value)
}
if (mirrorRef.value) {
resizeObserver.observe(mirrorRef.value)
}
})
onUnmounted(() => {
resizeObserver?.disconnect()
})
</script>
<style scoped>
.textarea-shell {
position: relative;
display: flex;
width: 320px;
height: 200px;
overflow: hidden;
resize: both;
}
textarea {
box-sizing: border-box;
width: 100%;
padding: 12px;
border: 0;
background: transparent;
font: 14px/1.6 system-ui;
}
.textarea-mirror {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 100%;
padding: 12px;
visibility: hidden;
white-space: pre;
pointer-events: none;
font: 14px/1.6 system-ui;
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
style |
CSSProperties |
undefined |
Styles applied to the default scroll container. |
containerRender |
ScrollAreaContainerRender |
undefined |
Custom container renderer for native elements such as textarea. |
guttersPosition |
'sticky' | 'absolute' |
'sticky' |
Positioning strategy for scrollbar gutters. Use absolute for custom containers that hide overflow. |
verticalScrollbarMinHeight |
number |
0 |
Minimum vertical scrollbar thumb height. |
horizontalScrollbarMinWidth |
number |
0 |
Minimum horizontal scrollbar thumb width. |
verticalScrollbarWidth |
number |
8 |
Vertical gutter width. |
horizontalScrollbarHeight |
number |
8 |
Horizontal gutter height. |
verticalScrollbarInsets |
DirectionInsets |
{ leading: 0, trailing: 8, top: 8, bottom: 8 } |
Insets for the vertical scrollbar. |
horizontalScrollbarInsets |
DirectionInsets |
{ leading: 8, trailing: 8, top: 0, bottom: 8 } |
Insets for the horizontal scrollbar. |
conflictedVerticalScrollbarInsets |
DirectionInsets |
{ leading: 0, trailing: 8, top: 8, bottom: 16 } |
Vertical insets when both scrollbars are visible. |
conflictedHorizontalScrollbarInsets |
DirectionInsets |
{ leading: 8, trailing: 16, top: 0, bottom: 8 } |
Horizontal insets when both scrollbars are visible. |
type DirectionInsets = {
leading: number
trailing: number
top: number
bottom: number
}corepack prepare
pnpm i
pnpm devIf the playground does not pick up library type or build changes, run:
pnpm build