0
# Scroll and Resize Directives
1
2
Directives for handling scroll events, resize observations, and scroll locking.
3
4
## Capabilities
5
6
### vScroll Directive
7
8
Directive for handling scroll events with customizable options.
9
10
```typescript { .api }
11
/**
12
* Directive for handling scroll events
13
* @example
14
* <div v-scroll="handleScroll">Scrollable content</div>
15
* <div v-scroll="[handleScroll, { throttle: 100 }]">With options</div>
16
*/
17
type ScrollHandler = (event: Event) => void;
18
19
interface VScrollValue {
20
/** Simple handler function */
21
handler: ScrollHandler;
22
/** Handler with options tuple */
23
handlerWithOptions: [ScrollHandler, UseScrollOptions];
24
}
25
26
interface UseScrollOptions {
27
/** Throttle scroll events (ms) @default 0 */
28
throttle?: number;
29
/** Idle timeout (ms) @default 200 */
30
idle?: number;
31
/** Offset threshold before triggering */
32
offset?: ScrollOffset;
33
/** Event listener options */
34
eventListenerOptions?: boolean | AddEventListenerOptions;
35
/** Behavior when element changes */
36
behavior?: ScrollBehavior;
37
/** On scroll callback */
38
onScroll?: (e: Event) => void;
39
/** On scroll end callback */
40
onStop?: (e: Event) => void;
41
}
42
43
interface ScrollOffset {
44
top?: number;
45
bottom?: number;
46
right?: number;
47
left?: number;
48
}
49
50
type ScrollBehavior = 'auto' | 'smooth';
51
```
52
53
**Usage Examples:**
54
55
```vue
56
<template>
57
<!-- Basic scroll handling -->
58
<div
59
v-scroll="handleScroll"
60
class="scroll-container"
61
style="height: 300px; overflow-y: auto;"
62
>
63
<div class="scroll-content">
64
<h3>Scrollable Content</h3>
65
<p v-for="i in 20" :key="i">
66
This is line {{ i }} of scrollable content. Scroll to see the handler in action.
67
</p>
68
</div>
69
</div>
70
71
<!-- Throttled scroll with options -->
72
<div
73
v-scroll="[handleThrottledScroll, { throttle: 100, idle: 500 }]"
74
class="throttled-scroll"
75
style="height: 200px; overflow-y: auto;"
76
>
77
<div class="scroll-info">
78
<p>Throttled scroll events (100ms)</p>
79
<p>Scroll position: {{ scrollPosition }}</p>
80
<p>Is scrolling: {{ isScrolling ? 'Yes' : 'No' }}</p>
81
</div>
82
<div v-for="i in 15" :key="i" class="scroll-item">
83
Item {{ i }}
84
</div>
85
</div>
86
87
<!-- Scroll with callbacks -->
88
<div
89
v-scroll="[null, {
90
onScroll: handleScrollEvent,
91
onStop: handleScrollStop,
92
offset: { top: 50, bottom: 50 }
93
}]"
94
class="callback-scroll"
95
style="height: 250px; overflow-y: auto;"
96
>
97
<div class="callback-info">
98
<p>Scroll with callbacks and offset</p>
99
<p>Scroll events: {{ scrollEventCount }}</p>
100
<p>Stop events: {{ stopEventCount }}</p>
101
</div>
102
<div v-for="i in 25" :key="i" class="callback-item">
103
Callback item {{ i }}
104
</div>
105
</div>
106
107
<!-- Window scroll tracking -->
108
<div v-scroll="handleWindowScroll">
109
<h3>Window Scroll Tracking</h3>
110
<p>Window scroll Y: {{ windowScrollY }}px</p>
111
<div style="height: 1500px; background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);">
112
<p style="position: sticky; top: 20px; padding: 20px; background: white; margin: 50px;">
113
Scroll the page to see window scroll tracking in action
114
</p>
115
</div>
116
</div>
117
</template>
118
119
<script setup>
120
import { ref } from 'vue';
121
import { vScroll } from '@vueuse/components';
122
123
const scrollPosition = ref(0);
124
const isScrolling = ref(false);
125
const scrollEventCount = ref(0);
126
const stopEventCount = ref(0);
127
const windowScrollY = ref(0);
128
129
function handleScroll(event) {
130
console.log('Scroll event:', event.target.scrollTop);
131
}
132
133
function handleThrottledScroll(event) {
134
scrollPosition.value = event.target.scrollTop;
135
isScrolling.value = true;
136
137
// Reset scrolling state after a delay
138
setTimeout(() => {
139
isScrolling.value = false;
140
}, 600);
141
}
142
143
function handleScrollEvent(event) {
144
scrollEventCount.value++;
145
}
146
147
function handleScrollStop(event) {
148
stopEventCount.value++;
149
}
150
151
function handleWindowScroll(event) {
152
windowScrollY.value = window.scrollY;
153
}
154
</script>
155
156
<style>
157
.scroll-container, .throttled-scroll, .callback-scroll {
158
border: 2px solid #ddd;
159
border-radius: 8px;
160
margin: 15px 0;
161
background: #f9f9f9;
162
}
163
164
.scroll-content, .scroll-info, .callback-info {
165
padding: 15px;
166
background: white;
167
margin-bottom: 10px;
168
border-radius: 6px;
169
}
170
171
.scroll-item, .callback-item {
172
padding: 10px 15px;
173
margin: 5px 15px;
174
background: white;
175
border-radius: 4px;
176
border-left: 3px solid #2196f3;
177
}
178
</style>
179
```
180
181
### vResizeObserver Directive
182
183
Directive for observing element size changes using ResizeObserver.
184
185
```typescript { .api }
186
/**
187
* Directive for observing element resize
188
* @example
189
* <div v-resize-observer="handleResize">Resizable element</div>
190
* <div v-resize-observer="[handleResize, { box: 'border-box' }]">With options</div>
191
*/
192
type ResizeObserverHandler = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
193
194
interface VResizeObserverValue {
195
/** Simple handler function */
196
handler: ResizeObserverHandler;
197
/** Handler with options tuple */
198
handlerWithOptions: [ResizeObserverHandler, UseResizeObserverOptions];
199
}
200
201
interface UseResizeObserverOptions {
202
/** ResizeObserver box type @default 'content-box' */
203
box?: ResizeObserverBoxOptions;
204
}
205
206
type ResizeObserverBoxOptions = 'border-box' | 'content-box' | 'device-pixel-content-box';
207
208
interface ResizeObserverEntry {
209
readonly borderBoxSize: ReadonlyArray<ResizeObserverSize>;
210
readonly contentBoxSize: ReadonlyArray<ResizeObserverSize>;
211
readonly contentRect: DOMRectReadOnly;
212
readonly devicePixelContentBoxSize: ReadonlyArray<ResizeObserverSize>;
213
readonly target: Element;
214
}
215
216
interface ResizeObserverSize {
217
readonly blockSize: number;
218
readonly inlineSize: number;
219
}
220
```
221
222
**Usage Examples:**
223
224
```vue
225
<template>
226
<!-- Basic resize observation -->
227
<div class="resize-demo">
228
<h3>Resize Observer Demo</h3>
229
<textarea
230
v-resize-observer="handleBasicResize"
231
v-model="textContent"
232
class="resizable-textarea"
233
placeholder="Resize this textarea to see the observer in action"
234
></textarea>
235
<p>Textarea size: {{ textareaSize.width }} ร {{ textareaSize.height }}</p>
236
</div>
237
238
<!-- Box model observation -->
239
<div class="box-model-demo">
240
<h3>Box Model Observation</h3>
241
<div
242
v-resize-observer="[handleBoxResize, { box: 'border-box' }]"
243
class="border-box-element"
244
:style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"
245
>
246
<div class="box-content">
247
<p>Border-box sizing</p>
248
<p>Size: {{ borderBoxSize.width }} ร {{ borderBoxSize.height }}</p>
249
</div>
250
</div>
251
<div class="size-controls">
252
<label>Width: <input v-model.number="boxWidth" type="range" min="200" max="500" /></label>
253
<label>Height: <input v-model.number="boxHeight" type="range" min="100" max="300" /></label>
254
</div>
255
</div>
256
257
<!-- Content box observation -->
258
<div class="content-box-demo">
259
<h3>Content Box vs Border Box</h3>
260
<div class="comparison">
261
<div
262
v-resize-observer="[handleContentBoxResize, { box: 'content-box' }]"
263
class="content-box"
264
>
265
<h4>Content Box</h4>
266
<p>Content: {{ contentBoxSize.width }} ร {{ contentBoxSize.height }}</p>
267
</div>
268
<div
269
v-resize-observer="[handleBorderBoxResize, { box: 'border-box' }]"
270
class="border-box"
271
>
272
<h4>Border Box</h4>
273
<p>Border: {{ borderBoxCompare.width }} ร {{ borderBoxCompare.height }}</p>
274
</div>
275
</div>
276
</div>
277
278
<!-- Responsive component -->
279
<div class="responsive-demo">
280
<h3>Responsive Component</h3>
281
<div
282
v-resize-observer="handleResponsiveResize"
283
class="responsive-container"
284
:class="responsiveClass"
285
>
286
<div class="responsive-content">
287
<h4>{{ responsiveTitle }}</h4>
288
<p>Container width: {{ containerWidth }}px</p>
289
<p>Layout: {{ responsiveClass }}</p>
290
</div>
291
</div>
292
<p class="responsive-info">
293
Resize the browser window to see responsive changes
294
</p>
295
</div>
296
</template>
297
298
<script setup>
299
import { ref, computed } from 'vue';
300
import { vResizeObserver } from '@vueuse/components';
301
302
const textContent = ref('Resize me!');
303
const textareaSize = ref({ width: 0, height: 0 });
304
const boxWidth = ref(300);
305
const boxHeight = ref(200);
306
const borderBoxSize = ref({ width: 0, height: 0 });
307
const contentBoxSize = ref({ width: 0, height: 0 });
308
const borderBoxCompare = ref({ width: 0, height: 0 });
309
const containerWidth = ref(0);
310
311
const responsiveClass = computed(() => {
312
if (containerWidth.value < 400) return 'mobile';
313
if (containerWidth.value < 768) return 'tablet';
314
return 'desktop';
315
});
316
317
const responsiveTitle = computed(() => {
318
switch (responsiveClass.value) {
319
case 'mobile': return 'Mobile Layout';
320
case 'tablet': return 'Tablet Layout';
321
case 'desktop': return 'Desktop Layout';
322
default: return 'Unknown Layout';
323
}
324
});
325
326
function handleBasicResize(entries) {
327
const entry = entries[0];
328
if (entry) {
329
textareaSize.value = {
330
width: Math.round(entry.contentRect.width),
331
height: Math.round(entry.contentRect.height)
332
};
333
}
334
}
335
336
function handleBoxResize(entries) {
337
const entry = entries[0];
338
if (entry && entry.borderBoxSize?.[0]) {
339
borderBoxSize.value = {
340
width: Math.round(entry.borderBoxSize[0].inlineSize),
341
height: Math.round(entry.borderBoxSize[0].blockSize)
342
};
343
}
344
}
345
346
function handleContentBoxResize(entries) {
347
const entry = entries[0];
348
if (entry && entry.contentBoxSize?.[0]) {
349
contentBoxSize.value = {
350
width: Math.round(entry.contentBoxSize[0].inlineSize),
351
height: Math.round(entry.contentBoxSize[0].blockSize)
352
};
353
}
354
}
355
356
function handleBorderBoxResize(entries) {
357
const entry = entries[0];
358
if (entry && entry.borderBoxSize?.[0]) {
359
borderBoxCompare.value = {
360
width: Math.round(entry.borderBoxSize[0].inlineSize),
361
height: Math.round(entry.borderBoxSize[0].blockSize)
362
};
363
}
364
}
365
366
function handleResponsiveResize(entries) {
367
const entry = entries[0];
368
if (entry) {
369
containerWidth.value = Math.round(entry.contentRect.width);
370
}
371
}
372
</script>
373
374
<style>
375
.resize-demo, .box-model-demo, .content-box-demo, .responsive-demo {
376
border: 1px solid #ddd;
377
border-radius: 8px;
378
padding: 20px;
379
margin: 15px 0;
380
}
381
382
.resizable-textarea {
383
width: 100%;
384
min-height: 100px;
385
padding: 10px;
386
border: 2px solid #ddd;
387
border-radius: 4px;
388
resize: both;
389
font-family: inherit;
390
}
391
392
.border-box-element {
393
border: 10px solid #2196f3;
394
padding: 20px;
395
background: #e3f2fd;
396
border-radius: 8px;
397
margin: 15px 0;
398
transition: all 0.3s;
399
box-sizing: border-box;
400
}
401
402
.box-content {
403
text-align: center;
404
}
405
406
.size-controls {
407
display: flex;
408
gap: 20px;
409
margin-top: 15px;
410
}
411
412
.size-controls label {
413
display: flex;
414
flex-direction: column;
415
gap: 5px;
416
}
417
418
.comparison {
419
display: grid;
420
grid-template-columns: 1fr 1fr;
421
gap: 20px;
422
margin-top: 15px;
423
}
424
425
.content-box, .border-box {
426
padding: 15px;
427
border: 5px solid #4caf50;
428
border-radius: 6px;
429
background: #e8f5e8;
430
text-align: center;
431
}
432
433
.responsive-container {
434
border: 2px solid #ddd;
435
border-radius: 8px;
436
padding: 20px;
437
margin: 15px 0;
438
transition: all 0.3s;
439
min-height: 150px;
440
display: flex;
441
align-items: center;
442
justify-content: center;
443
}
444
445
.responsive-container.mobile {
446
background: #ffebee;
447
border-color: #f44336;
448
}
449
450
.responsive-container.tablet {
451
background: #e8f5e8;
452
border-color: #4caf50;
453
}
454
455
.responsive-container.desktop {
456
background: #e3f2fd;
457
border-color: #2196f3;
458
}
459
460
.responsive-content {
461
text-align: center;
462
}
463
464
.responsive-info {
465
font-size: 0.9em;
466
color: #666;
467
font-style: italic;
468
text-align: center;
469
}
470
</style>
471
```
472
473
### vScrollLock Directive
474
475
Directive for preventing or controlling scroll behavior.
476
477
```typescript { .api }
478
/**
479
* Directive for scroll locking
480
* @example
481
* <div v-scroll-lock="true">Scroll locked</div>
482
* <div v-scroll-lock="scrollLockOptions">With options</div>
483
*/
484
interface VScrollLockValue {
485
/** Simple boolean to enable/disable */
486
enabled: boolean;
487
/** Options object for advanced control */
488
options: ScrollLockOptions;
489
}
490
491
interface ScrollLockOptions {
492
/** Whether scroll lock is enabled @default true */
493
enabled?: boolean;
494
/** Preserve scroll position @default true */
495
preserveScrollBarGap?: boolean;
496
/** Allow touch move on target @default false */
497
allowTouchMove?: (el: EventTarget | null) => boolean;
498
}
499
```
500
501
**Usage Examples:**
502
503
```vue
504
<template>
505
<!-- Basic scroll lock toggle -->
506
<div class="scroll-lock-demo">
507
<h3>Scroll Lock Demo</h3>
508
<div class="lock-controls">
509
<label class="lock-toggle">
510
<input v-model="isLocked" type="checkbox" />
511
<span>Lock page scroll</span>
512
</label>
513
</div>
514
<div v-scroll-lock="isLocked" class="lock-indicator" :class="{ locked: isLocked }">
515
{{ isLocked ? '๐ Page scroll is locked' : '๐ Page scroll is unlocked' }}
516
</div>
517
<p class="lock-instructions">
518
Toggle the checkbox and try scrolling the page to see the effect.
519
</p>
520
</div>
521
522
<!-- Modal with scroll lock -->
523
<div class="modal-demo">
524
<h3>Modal with Scroll Lock</h3>
525
<button @click="showModal = true" class="open-modal-btn">
526
Open Modal
527
</button>
528
529
<div v-if="showModal" class="modal-overlay" @click="closeModal">
530
<div v-scroll-lock="showModal" class="modal-content" @click.stop>
531
<h4>Modal Dialog</h4>
532
<p>The page scroll is locked while this modal is open.</p>
533
<div class="modal-scroll-area">
534
<p v-for="i in 20" :key="i">
535
This is scrollable content inside the modal: Line {{ i }}
536
</p>
537
</div>
538
<div class="modal-actions">
539
<button @click="closeModal" class="close-btn">Close Modal</button>
540
</div>
541
</div>
542
</div>
543
</div>
544
545
<!-- Advanced scroll lock with options -->
546
<div class="advanced-lock-demo">
547
<h3>Advanced Scroll Lock</h3>
548
<div class="advanced-controls">
549
<label>
550
<input v-model="advancedLock.enabled" type="checkbox" />
551
Enable Advanced Lock
552
</label>
553
<label>
554
<input v-model="advancedLock.preserveScrollBarGap" type="checkbox" />
555
Preserve Scrollbar Gap
556
</label>
557
</div>
558
559
<div
560
v-scroll-lock="advancedLockOptions"
561
class="advanced-lock-area"
562
:class="{ locked: advancedLock.enabled }"
563
>
564
<h4>Advanced Lock Area</h4>
565
<p>Lock status: {{ advancedLock.enabled ? 'Active' : 'Inactive' }}</p>
566
<p>Scrollbar gap preserved: {{ advancedLock.preserveScrollBarGap ? 'Yes' : 'No' }}</p>
567
568
<div class="scrollable-section">
569
<h5>Allowed Scroll Area</h5>
570
<div class="allowed-scroll-content">
571
<p v-for="i in 15" :key="i">
572
This content can still be scrolled: Item {{ i }}
573
</p>
574
</div>
575
</div>
576
</div>
577
</div>
578
579
<!-- Conditional scroll lock -->
580
<div class="conditional-demo">
581
<h3>Conditional Scroll Lock</h3>
582
<div class="condition-controls">
583
<button @click="toggleSidebar" class="sidebar-toggle">
584
{{ sidebarOpen ? 'Close' : 'Open' }} Sidebar
585
</button>
586
</div>
587
588
<div class="layout-container">
589
<div
590
v-scroll-lock="sidebarOpen && isMobile"
591
class="sidebar"
592
:class="{ open: sidebarOpen, mobile: isMobile }"
593
>
594
<h4>Sidebar</h4>
595
<nav class="sidebar-nav">
596
<a href="#" v-for="i in 10" :key="i">Navigation Item {{ i }}</a>
597
</nav>
598
</div>
599
600
<div class="main-content">
601
<h4>Main Content</h4>
602
<p>Sidebar open: {{ sidebarOpen }}</p>
603
<p>Mobile mode: {{ isMobile }}</p>
604
<p>Scroll locked: {{ sidebarOpen && isMobile }}</p>
605
<div class="content-scroll">
606
<p v-for="i in 30" :key="i">
607
Main content line {{ i }}. On mobile, opening the sidebar locks scroll.
608
</p>
609
</div>
610
</div>
611
</div>
612
</div>
613
</template>
614
615
<script setup>
616
import { ref, computed, onMounted, onUnmounted } from 'vue';
617
import { vScrollLock } from '@vueuse/components';
618
619
const isLocked = ref(false);
620
const showModal = ref(false);
621
const sidebarOpen = ref(false);
622
const windowWidth = ref(window.innerWidth);
623
624
const advancedLock = ref({
625
enabled: false,
626
preserveScrollBarGap: true
627
});
628
629
const advancedLockOptions = computed(() => ({
630
enabled: advancedLock.value.enabled,
631
preserveScrollBarGap: advancedLock.value.preserveScrollBarGap,
632
allowTouchMove: (el) => {
633
// Allow touch move on elements with 'scrollable' class
634
return el?.closest?.('.scrollable-section') !== null;
635
}
636
}));
637
638
const isMobile = computed(() => windowWidth.value < 768);
639
640
function closeModal() {
641
showModal.value = false;
642
}
643
644
function toggleSidebar() {
645
sidebarOpen.value = !sidebarOpen.value;
646
}
647
648
function updateWindowWidth() {
649
windowWidth.value = window.innerWidth;
650
}
651
652
onMounted(() => {
653
window.addEventListener('resize', updateWindowWidth);
654
});
655
656
onUnmounted(() => {
657
window.removeEventListener('resize', updateWindowWidth);
658
});
659
</script>
660
661
<style>
662
.scroll-lock-demo, .modal-demo, .advanced-lock-demo, .conditional-demo {
663
border: 1px solid #ddd;
664
border-radius: 8px;
665
padding: 20px;
666
margin: 15px 0;
667
}
668
669
.lock-controls, .advanced-controls, .condition-controls {
670
margin: 15px 0;
671
}
672
673
.lock-toggle {
674
display: flex;
675
align-items: center;
676
gap: 8px;
677
cursor: pointer;
678
}
679
680
.lock-indicator {
681
padding: 15px;
682
border-radius: 6px;
683
text-align: center;
684
font-weight: bold;
685
margin: 15px 0;
686
}
687
688
.lock-indicator.locked {
689
background: #ffebee;
690
color: #c62828;
691
}
692
693
.lock-indicator:not(.locked) {
694
background: #e8f5e8;
695
color: #2e7d32;
696
}
697
698
.lock-instructions {
699
font-size: 0.9em;
700
color: #666;
701
font-style: italic;
702
}
703
704
.open-modal-btn, .close-btn, .sidebar-toggle {
705
padding: 10px 20px;
706
border: none;
707
border-radius: 6px;
708
cursor: pointer;
709
font-size: 16px;
710
}
711
712
.open-modal-btn, .sidebar-toggle {
713
background: #2196f3;
714
color: white;
715
}
716
717
.modal-overlay {
718
position: fixed;
719
top: 0;
720
left: 0;
721
right: 0;
722
bottom: 0;
723
background: rgba(0, 0, 0, 0.7);
724
display: flex;
725
align-items: center;
726
justify-content: center;
727
z-index: 1000;
728
}
729
730
.modal-content {
731
background: white;
732
border-radius: 8px;
733
padding: 20px;
734
max-width: 500px;
735
max-height: 80vh;
736
overflow-y: auto;
737
margin: 20px;
738
}
739
740
.modal-scroll-area {
741
max-height: 200px;
742
overflow-y: auto;
743
border: 1px solid #ddd;
744
padding: 10px;
745
margin: 15px 0;
746
border-radius: 4px;
747
}
748
749
.modal-actions {
750
text-align: center;
751
margin-top: 20px;
752
}
753
754
.close-btn {
755
background: #f44336;
756
color: white;
757
}
758
759
.advanced-controls {
760
display: flex;
761
flex-direction: column;
762
gap: 10px;
763
}
764
765
.advanced-controls label {
766
display: flex;
767
align-items: center;
768
gap: 8px;
769
}
770
771
.advanced-lock-area {
772
border: 2px solid #ddd;
773
border-radius: 6px;
774
padding: 15px;
775
margin: 15px 0;
776
transition: border-color 0.3s;
777
}
778
779
.advanced-lock-area.locked {
780
border-color: #f44336;
781
background: #fafafa;
782
}
783
784
.scrollable-section {
785
margin-top: 15px;
786
border: 1px solid #ccc;
787
border-radius: 4px;
788
overflow: hidden;
789
}
790
791
.allowed-scroll-content {
792
height: 150px;
793
overflow-y: auto;
794
padding: 10px;
795
background: white;
796
}
797
798
.layout-container {
799
display: flex;
800
gap: 0;
801
min-height: 300px;
802
border: 1px solid #ddd;
803
border-radius: 6px;
804
overflow: hidden;
805
}
806
807
.sidebar {
808
width: 250px;
809
background: #f5f5f5;
810
border-right: 1px solid #ddd;
811
padding: 15px;
812
transform: translateX(-100%);
813
transition: transform 0.3s;
814
}
815
816
.sidebar.open {
817
transform: translateX(0);
818
}
819
820
.sidebar.mobile.open {
821
position: fixed;
822
top: 0;
823
left: 0;
824
bottom: 0;
825
z-index: 100;
826
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
827
}
828
829
.sidebar-nav {
830
display: flex;
831
flex-direction: column;
832
gap: 8px;
833
}
834
835
.sidebar-nav a {
836
padding: 8px 12px;
837
text-decoration: none;
838
color: #333;
839
border-radius: 4px;
840
transition: background 0.2s;
841
}
842
843
.sidebar-nav a:hover {
844
background: #e0e0e0;
845
}
846
847
.main-content {
848
flex: 1;
849
padding: 15px;
850
}
851
852
.content-scroll {
853
max-height: 200px;
854
overflow-y: auto;
855
border: 1px solid #ddd;
856
padding: 10px;
857
margin-top: 10px;
858
border-radius: 4px;
859
}
860
</style>
861
```
862
863
### vInfiniteScroll Directive
864
865
Directive for implementing infinite scroll functionality.
866
867
```typescript { .api }
868
/**
869
* Directive for infinite scroll implementation
870
* @example
871
* <div v-infinite-scroll="loadMore">Scroll to load more</div>
872
* <div v-infinite-scroll="[loadMore, { distance: 100 }]">With options</div>
873
*/
874
type InfiniteScrollHandler = () => void | Promise<void>;
875
876
interface VInfiniteScrollValue {
877
/** Simple handler function */
878
handler: InfiniteScrollHandler;
879
/** Handler with options tuple */
880
handlerWithOptions: [InfiniteScrollHandler, UseInfiniteScrollOptions];
881
}
882
883
interface UseInfiniteScrollOptions {
884
/** Distance from bottom to trigger @default 0 */
885
distance?: number;
886
/** Direction to observe @default 'bottom' */
887
direction?: 'top' | 'bottom' | 'left' | 'right';
888
/** Preserve scroll position when new content added @default false */
889
preserveScrollPosition?: boolean;
890
/** Throttle scroll events (ms) @default 0 */
891
throttle?: number;
892
/** Whether infinite scroll is enabled @default true */
893
canLoadMore?: () => boolean;
894
}
895
```
896
897
**Usage Examples:**
898
899
```vue
900
<template>
901
<!-- Basic infinite scroll -->
902
<div class="infinite-demo">
903
<h3>Basic Infinite Scroll</h3>
904
<div
905
v-infinite-scroll="loadMoreItems"
906
class="infinite-container"
907
>
908
<div class="infinite-list">
909
<div v-for="item in items" :key="item.id" class="infinite-item">
910
<span class="item-id">#{{ item.id }}</span>
911
<span class="item-name">{{ item.name }}</span>
912
<span class="item-description">{{ item.description }}</span>
913
</div>
914
</div>
915
<div v-if="isLoading" class="loading-indicator">
916
Loading more items...
917
</div>
918
<div v-if="hasReachedEnd" class="end-indicator">
919
No more items to load
920
</div>
921
</div>
922
</div>
923
924
<!-- Infinite scroll with distance -->
925
<div class="distance-demo">
926
<h3>Infinite Scroll with Distance</h3>
927
<div
928
v-infinite-scroll="[loadMorePhotos, { distance: 200, throttle: 300 }]"
929
class="photos-container"
930
>
931
<div class="photos-grid">
932
<div v-for="photo in photos" :key="photo.id" class="photo-item">
933
<img :src="photo.thumbnail" :alt="photo.title" class="photo-image" />
934
<div class="photo-info">
935
<h5>{{ photo.title }}</h5>
936
<p>{{ photo.description }}</p>
937
</div>
938
</div>
939
</div>
940
<div class="load-info">
941
<p>Photos loaded: {{ photos.length }}</p>
942
<p>Scroll position: {{ Math.round(scrollPosition) }}px from bottom</p>
943
<p v-if="photoLoading" class="loading-text">๐ธ Loading more photos...</p>
944
</div>
945
</div>
946
</div>
947
948
<!-- Bidirectional infinite scroll -->
949
<div class="bidirectional-demo">
950
<h3>Bidirectional Infinite Scroll</h3>
951
<div class="chat-container">
952
<!-- Load older messages (top) -->
953
<div
954
v-infinite-scroll="[loadOlderMessages, { direction: 'top', distance: 100 }]"
955
class="older-loader"
956
>
957
<div v-if="loadingOlder" class="loading-older">
958
Loading older messages...
959
</div>
960
</div>
961
962
<!-- Messages list -->
963
<div class="messages-list">
964
<div v-for="message in messages" :key="message.id" class="message-item">
965
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
966
<div class="message-content">{{ message.content }}</div>
967
</div>
968
</div>
969
970
<!-- Load newer messages (bottom) -->
971
<div
972
v-infinite-scroll="[loadNewerMessages, { direction: 'bottom', distance: 50 }]"
973
class="newer-loader"
974
>
975
<div v-if="loadingNewer" class="loading-newer">
976
Loading newer messages...
977
</div>
978
</div>
979
</div>
980
</div>
981
982
<!-- Conditional infinite scroll -->
983
<div class="conditional-demo">
984
<h3>Conditional Infinite Scroll</h3>
985
<div class="scroll-controls">
986
<label>
987
<input v-model="canLoadMore" type="checkbox" />
988
Enable infinite scroll
989
</label>
990
<button @click="resetData" class="reset-btn">Reset Data</button>
991
</div>
992
<div
993
v-infinite-scroll="[loadConditionalData, {
994
distance: 150,
995
canLoadMore: () => canLoadMore && !dataEnded
996
}]"
997
class="conditional-container"
998
:class="{ disabled: !canLoadMore }"
999
>
1000
<div class="data-list">
1001
<div v-for="item in conditionalData" :key="item.id" class="data-item">
1002
{{ item.name }} - {{ item.value }}
1003
</div>
1004
</div>
1005
<div class="conditional-status">
1006
<p>Items: {{ conditionalData.length }}</p>
1007
<p>Can load more: {{ canLoadMore && !dataEnded ? 'Yes' : 'No' }}</p>
1008
<p v-if="conditionalLoading" class="loading-text">Loading...</p>
1009
<p v-if="dataEnded" class="end-text">All data loaded</p>
1010
</div>
1011
</div>
1012
</div>
1013
</template>
1014
1015
<script setup>
1016
import { ref, computed } from 'vue';
1017
import { vInfiniteScroll } from '@vueuse/components';
1018
1019
// Basic infinite scroll data
1020
const items = ref([]);
1021
const isLoading = ref(false);
1022
const itemIdCounter = ref(1);
1023
const hasReachedEnd = ref(false);
1024
1025
// Photos data
1026
const photos = ref([]);
1027
const photoLoading = ref(false);
1028
const photoIdCounter = ref(1);
1029
const scrollPosition = ref(0);
1030
1031
// Messages data
1032
const messages = ref([]);
1033
const loadingOlder = ref(false);
1034
const loadingNewer = ref(false);
1035
const oldestMessageId = ref(100);
1036
const newestMessageId = ref(150);
1037
1038
// Conditional data
1039
const conditionalData = ref([]);
1040
const conditionalLoading = ref(false);
1041
const canLoadMore = ref(true);
1042
const dataEnded = ref(false);
1043
const conditionalIdCounter = ref(1);
1044
1045
// Initialize with some data
1046
initializeData();
1047
1048
async function loadMoreItems() {
1049
if (isLoading.value || hasReachedEnd.value) return;
1050
1051
isLoading.value = true;
1052
1053
// Simulate API delay
1054
await new Promise(resolve => setTimeout(resolve, 1000));
1055
1056
const newItems = [];
1057
for (let i = 0; i < 10; i++) {
1058
newItems.push({
1059
id: itemIdCounter.value++,
1060
name: `Item ${itemIdCounter.value - 1}`,
1061
description: `Description for item ${itemIdCounter.value - 1}`
1062
});
1063
}
1064
1065
items.value.push(...newItems);
1066
isLoading.value = false;
1067
1068
// Simulate reaching end after 50 items
1069
if (items.value.length >= 50) {
1070
hasReachedEnd.value = true;
1071
}
1072
}
1073
1074
async function loadMorePhotos() {
1075
if (photoLoading.value) return;
1076
1077
photoLoading.value = true;
1078
1079
await new Promise(resolve => setTimeout(resolve, 800));
1080
1081
const newPhotos = [];
1082
for (let i = 0; i < 6; i++) {
1083
const id = photoIdCounter.value++;
1084
newPhotos.push({
1085
id,
1086
title: `Photo ${id}`,
1087
description: `Beautiful photo #${id}`,
1088
thumbnail: `https://picsum.photos/200/200?random=${id}`
1089
});
1090
}
1091
1092
photos.value.push(...newPhotos);
1093
photoLoading.value = false;
1094
}
1095
1096
async function loadOlderMessages() {
1097
if (loadingOlder.value) return;
1098
1099
loadingOlder.value = true;
1100
1101
await new Promise(resolve => setTimeout(resolve, 600));
1102
1103
const olderMessages = [];
1104
for (let i = 0; i < 5; i++) {
1105
olderMessages.unshift({
1106
id: --oldestMessageId.value,
1107
timestamp: Date.now() - (100 - oldestMessageId.value) * 60000,
1108
content: `Older message ${oldestMessageId.value + 1}`
1109
});
1110
}
1111
1112
messages.value.unshift(...olderMessages);
1113
loadingOlder.value = false;
1114
}
1115
1116
async function loadNewerMessages() {
1117
if (loadingNewer.value) return;
1118
1119
loadingNewer.value = true;
1120
1121
await new Promise(resolve => setTimeout(resolve, 600));
1122
1123
const newerMessages = [];
1124
for (let i = 0; i < 5; i++) {
1125
newerMessages.push({
1126
id: ++newestMessageId.value,
1127
timestamp: Date.now() - (200 - newestMessageId.value) * 30000,
1128
content: `Newer message ${newestMessageId.value}`
1129
});
1130
}
1131
1132
messages.value.push(...newerMessages);
1133
loadingNewer.value = false;
1134
}
1135
1136
async function loadConditionalData() {
1137
if (conditionalLoading.value || !canLoadMore.value || dataEnded.value) return;
1138
1139
conditionalLoading.value = true;
1140
1141
await new Promise(resolve => setTimeout(resolve, 700));
1142
1143
const newData = [];
1144
for (let i = 0; i < 8; i++) {
1145
if (conditionalData.value.length >= 40) {
1146
dataEnded.value = true;
1147
break;
1148
}
1149
1150
newData.push({
1151
id: conditionalIdCounter.value++,
1152
name: `Data Item ${conditionalIdCounter.value - 1}`,
1153
value: Math.floor(Math.random() * 1000)
1154
});
1155
}
1156
1157
conditionalData.value.push(...newData);
1158
conditionalLoading.value = false;
1159
}
1160
1161
function initializeData() {
1162
// Initialize items
1163
for (let i = 0; i < 20; i++) {
1164
items.value.push({
1165
id: itemIdCounter.value++,
1166
name: `Item ${itemIdCounter.value - 1}`,
1167
description: `Description for item ${itemIdCounter.value - 1}`
1168
});
1169
}
1170
1171
// Initialize photos
1172
for (let i = 0; i < 12; i++) {
1173
const id = photoIdCounter.value++;
1174
photos.value.push({
1175
id,
1176
title: `Photo ${id}`,
1177
description: `Beautiful photo #${id}`,
1178
thumbnail: `https://picsum.photos/200/200?random=${id}`
1179
});
1180
}
1181
1182
// Initialize messages
1183
for (let i = 0; i < 20; i++) {
1184
const id = 120 + i;
1185
messages.value.push({
1186
id,
1187
timestamp: Date.now() - (150 - id) * 60000,
1188
content: `Message ${id}`
1189
});
1190
}
1191
1192
// Initialize conditional data
1193
for (let i = 0; i < 15; i++) {
1194
conditionalData.value.push({
1195
id: conditionalIdCounter.value++,
1196
name: `Data Item ${conditionalIdCounter.value - 1}`,
1197
value: Math.floor(Math.random() * 1000)
1198
});
1199
}
1200
}
1201
1202
function formatTime(timestamp) {
1203
return new Date(timestamp).toLocaleTimeString();
1204
}
1205
1206
function resetData() {
1207
conditionalData.value = [];
1208
conditionalIdCounter.value = 1;
1209
dataEnded.value = false;
1210
conditionalLoading.value = false;
1211
1212
// Re-initialize
1213
for (let i = 0; i < 15; i++) {
1214
conditionalData.value.push({
1215
id: conditionalIdCounter.value++,
1216
name: `Data Item ${conditionalIdCounter.value - 1}`,
1217
value: Math.floor(Math.random() * 1000)
1218
});
1219
}
1220
}
1221
</script>
1222
1223
<style>
1224
.infinite-demo, .distance-demo, .bidirectional-demo, .conditional-demo {
1225
border: 1px solid #ddd;
1226
border-radius: 8px;
1227
padding: 20px;
1228
margin: 15px 0;
1229
}
1230
1231
.infinite-container, .photos-container, .chat-container, .conditional-container {
1232
height: 400px;
1233
border: 2px solid #eee;
1234
border-radius: 6px;
1235
overflow-y: auto;
1236
background: #fafafa;
1237
}
1238
1239
.infinite-list, .data-list {
1240
padding: 10px;
1241
}
1242
1243
.infinite-item, .data-item {
1244
display: flex;
1245
align-items: center;
1246
gap: 15px;
1247
padding: 10px;
1248
margin: 8px 0;
1249
background: white;
1250
border-radius: 6px;
1251
border-left: 3px solid #2196f3;
1252
}
1253
1254
.item-id {
1255
font-weight: bold;
1256
color: #666;
1257
min-width: 50px;
1258
}
1259
1260
.item-name {
1261
font-weight: bold;
1262
min-width: 100px;
1263
}
1264
1265
.item-description {
1266
color: #666;
1267
flex: 1;
1268
}
1269
1270
.loading-indicator, .end-indicator, .loading-text, .end-text {
1271
text-align: center;
1272
padding: 15px;
1273
font-style: italic;
1274
}
1275
1276
.loading-indicator, .loading-text {
1277
color: #2196f3;
1278
}
1279
1280
.end-indicator, .end-text {
1281
color: #666;
1282
}
1283
1284
.photos-grid {
1285
display: grid;
1286
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1287
gap: 15px;
1288
padding: 15px;
1289
}
1290
1291
.photo-item {
1292
background: white;
1293
border-radius: 8px;
1294
overflow: hidden;
1295
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1296
}
1297
1298
.photo-image {
1299
width: 100%;
1300
height: 150px;
1301
object-fit: cover;
1302
}
1303
1304
.photo-info {
1305
padding: 10px;
1306
}
1307
1308
.photo-info h5 {
1309
margin: 0 0 5px 0;
1310
font-size: 14px;
1311
}
1312
1313
.photo-info p {
1314
margin: 0;
1315
font-size: 12px;
1316
color: #666;
1317
}
1318
1319
.load-info {
1320
padding: 15px;
1321
background: white;
1322
margin: 15px;
1323
border-radius: 6px;
1324
text-align: center;
1325
font-size: 14px;
1326
}
1327
1328
.messages-list {
1329
padding: 10px;
1330
flex: 1;
1331
}
1332
1333
.message-item {
1334
padding: 8px 12px;
1335
margin: 5px 0;
1336
background: white;
1337
border-radius: 6px;
1338
border-left: 2px solid #4caf50;
1339
}
1340
1341
.message-time {
1342
font-size: 12px;
1343
color: #666;
1344
margin-bottom: 4px;
1345
}
1346
1347
.message-content {
1348
font-size: 14px;
1349
}
1350
1351
.loading-older, .loading-newer {
1352
text-align: center;
1353
padding: 10px;
1354
color: #2196f3;
1355
font-style: italic;
1356
font-size: 14px;
1357
}
1358
1359
.scroll-controls {
1360
margin: 15px 0;
1361
display: flex;
1362
align-items: center;
1363
gap: 15px;
1364
}
1365
1366
.scroll-controls label {
1367
display: flex;
1368
align-items: center;
1369
gap: 8px;
1370
}
1371
1372
.reset-btn {
1373
padding: 6px 12px;
1374
border: 1px solid #ddd;
1375
border-radius: 4px;
1376
cursor: pointer;
1377
background: #f5f5f5;
1378
}
1379
1380
.conditional-container.disabled {
1381
opacity: 0.6;
1382
border-color: #ccc;
1383
}
1384
1385
.conditional-status {
1386
padding: 15px;
1387
background: white;
1388
margin: 10px;
1389
border-radius: 6px;
1390
font-size: 14px;
1391
text-align: center;
1392
}
1393
</style>
1394
```
1395
1396
## Type Definitions
1397
1398
```typescript { .api }
1399
/** Common types used across scroll and resize directives */
1400
type MaybeRefOrGetter<T> = T | Ref<T> | (() => T);
1401
1402
/** Scroll event and options */
1403
interface ScrollEventOptions extends AddEventListenerOptions {
1404
passive?: boolean;
1405
capture?: boolean;
1406
}
1407
1408
/** ResizeObserver types */
1409
interface ResizeObserver {
1410
disconnect(): void;
1411
observe(target: Element, options?: ResizeObserverOptions): void;
1412
unobserve(target: Element): void;
1413
}
1414
1415
interface ResizeObserverOptions {
1416
box?: ResizeObserverBoxOptions;
1417
}
1418
1419
/** Scroll lock types */
1420
interface ScrollLockState {
1421
isLocked: boolean;
1422
originalOverflow: string;
1423
originalPaddingRight: string;
1424
}
1425
1426
/** Infinite scroll directions */
1427
type InfiniteScrollDirection = 'top' | 'bottom' | 'left' | 'right';
1428
```