or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-apis.mdbrowser-events.mddevice-sensors.mdelement-tracking.mdindex.mdmouse-pointer.mdscroll-resize.mdtheme-preferences.mdutilities-advanced.mdwindow-document.md

scroll-resize.mddocs/

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

```