0
# Scrolling & Layout
1
2
Viewport tracking, scroll utilities, element positioning, and resize observation for responsive React components.
3
4
## Capabilities
5
6
### Scroll Parent Detection
7
8
Utilities for finding scrollable ancestors and determining scroll behavior.
9
10
```typescript { .api }
11
/**
12
* Finds the nearest scrollable ancestor element
13
* @param node - Starting element
14
* @param checkForOverflow - Whether to check overflow styles (default: true)
15
* @returns Scrollable parent element or document scrolling element
16
*/
17
function getScrollParent(node: Element, checkForOverflow?: boolean): Element;
18
19
/**
20
* Gets array of all scrollable ancestors
21
* @param node - Starting element
22
* @returns Array of scrollable parent elements
23
*/
24
function getScrollParents(node: Element): Element[];
25
26
/**
27
* Determines if element is scrollable
28
* @param element - Element to check
29
* @param checkForOverflow - Whether to check overflow styles
30
* @returns true if element can scroll
31
*/
32
function isScrollable(element: Element, checkForOverflow?: boolean): boolean;
33
```
34
35
**Usage Examples:**
36
37
```typescript
38
import { getScrollParent, getScrollParents, isScrollable } from "@react-aria/utils";
39
40
function ScrollAwareComponent() {
41
const elementRef = useRef<HTMLDivElement>(null);
42
const [scrollParent, setScrollParent] = useState<Element | null>(null);
43
44
useEffect(() => {
45
if (elementRef.current) {
46
// Find immediate scroll parent
47
const parent = getScrollParent(elementRef.current);
48
setScrollParent(parent);
49
50
// Get all scroll parents for complex layouts
51
const allParents = getScrollParents(elementRef.current);
52
console.log('All scroll parents:', allParents);
53
54
// Check if element itself is scrollable
55
const canScroll = isScrollable(elementRef.current);
56
console.log('Element can scroll:', canScroll);
57
}
58
}, []);
59
60
return (
61
<div ref={elementRef}>
62
Content that needs scroll awareness
63
{scrollParent && (
64
<p>Scroll parent: {scrollParent.tagName}</p>
65
)}
66
</div>
67
);
68
}
69
70
// Sticky positioning with scroll parent awareness
71
function StickyHeader() {
72
const headerRef = useRef<HTMLElement>(null);
73
const [isSticky, setIsSticky] = useState(false);
74
75
useEffect(() => {
76
if (!headerRef.current) return;
77
78
const scrollParent = getScrollParent(headerRef.current);
79
80
const handleScroll = () => {
81
const scrollTop = scrollParent.scrollTop || 0;
82
setIsSticky(scrollTop > 100);
83
};
84
85
scrollParent.addEventListener('scroll', handleScroll, { passive: true });
86
return () => scrollParent.removeEventListener('scroll', handleScroll);
87
}, []);
88
89
return (
90
<header
91
ref={headerRef}
92
className={isSticky ? 'sticky' : ''}
93
>
94
Header Content
95
</header>
96
);
97
}
98
```
99
100
### Scroll Into View
101
102
Utilities for scrolling elements into view with smart positioning.
103
104
```typescript { .api }
105
/**
106
* Scrolls container so element is visible (like {block: 'nearest'})
107
* @param scrollView - Container element to scroll
108
* @param element - Element to bring into view
109
*/
110
function scrollIntoView(scrollView: HTMLElement, element: HTMLElement): void;
111
112
/**
113
* Scrolls element into viewport with overlay awareness
114
* @param targetElement - Element to scroll into view
115
* @param opts - Options for scrolling behavior
116
*/
117
function scrollIntoViewport(
118
targetElement: Element,
119
opts?: { containingElement?: Element }
120
): void;
121
```
122
123
**Usage Examples:**
124
125
```typescript
126
import { scrollIntoView, scrollIntoViewport } from "@react-aria/utils";
127
128
function ScrollableList({ items, selectedIndex }) {
129
const listRef = useRef<HTMLUListElement>(null);
130
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
131
132
// Scroll selected item into view when selection changes
133
useEffect(() => {
134
if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && listRef.current) {
135
scrollIntoView(listRef.current, itemRefs.current[selectedIndex]!);
136
}
137
}, [selectedIndex]);
138
139
return (
140
<ul ref={listRef} className="scrollable-list">
141
{items.map((item, index) => (
142
<li
143
key={item.id}
144
ref={el => itemRefs.current[index] = el}
145
className={index === selectedIndex ? 'selected' : ''}
146
>
147
{item.name}
148
</li>
149
))}
150
</ul>
151
);
152
}
153
154
// Modal with smart scrolling
155
function Modal({ isOpen, children }) {
156
const modalRef = useRef<HTMLDivElement>(null);
157
158
useEffect(() => {
159
if (isOpen && modalRef.current) {
160
// Scroll modal into viewport, accounting for overlays
161
scrollIntoViewport(modalRef.current);
162
}
163
}, [isOpen]);
164
165
return isOpen ? (
166
<div className="modal-backdrop">
167
<div ref={modalRef} className="modal-content">
168
{children}
169
</div>
170
</div>
171
) : null;
172
}
173
174
// Keyboard navigation with scroll
175
function useKeyboardNavigation(items: any[], onSelect: (index: number) => void) {
176
const [selectedIndex, setSelectedIndex] = useState(0);
177
const containerRef = useRef<HTMLElement>(null);
178
179
const handleKeyDown = useCallback((e: KeyboardEvent) => {
180
switch (e.key) {
181
case 'ArrowDown':
182
e.preventDefault();
183
setSelectedIndex(prev => {
184
const newIndex = Math.min(prev + 1, items.length - 1);
185
186
// Scroll item into view
187
const container = containerRef.current;
188
const item = container?.children[newIndex] as HTMLElement;
189
if (container && item) {
190
scrollIntoView(container, item);
191
}
192
193
return newIndex;
194
});
195
break;
196
197
case 'ArrowUp':
198
e.preventDefault();
199
setSelectedIndex(prev => {
200
const newIndex = Math.max(prev - 1, 0);
201
202
const container = containerRef.current;
203
const item = container?.children[newIndex] as HTMLElement;
204
if (container && item) {
205
scrollIntoView(container, item);
206
}
207
208
return newIndex;
209
});
210
break;
211
212
case 'Enter':
213
e.preventDefault();
214
onSelect(selectedIndex);
215
break;
216
}
217
}, [items.length, selectedIndex, onSelect]);
218
219
return { selectedIndex, containerRef, handleKeyDown };
220
}
221
```
222
223
### Viewport Size Tracking
224
225
Hook for tracking viewport dimensions with device-aware updates.
226
227
```typescript { .api }
228
/**
229
* Tracks viewport dimensions with device-aware updates
230
* @returns Object with current viewport width and height
231
*/
232
function useViewportSize(): ViewportSize;
233
234
interface ViewportSize {
235
width: number;
236
height: number;
237
}
238
```
239
240
**Usage Examples:**
241
242
```typescript
243
import { useViewportSize } from "@react-aria/utils";
244
245
function ResponsiveComponent() {
246
const { width, height } = useViewportSize();
247
248
const isMobile = width < 768;
249
const isTablet = width >= 768 && width < 1024;
250
const isDesktop = width >= 1024;
251
252
return (
253
<div>
254
<p>Viewport: {width} x {height}</p>
255
<div className={`layout ${isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop'}`}>
256
{isMobile ? (
257
<MobileLayout />
258
) : isTablet ? (
259
<TabletLayout />
260
) : (
261
<DesktopLayout />
262
)}
263
</div>
264
</div>
265
);
266
}
267
268
// Responsive grid based on viewport
269
function ResponsiveGrid({ children }) {
270
const { width } = useViewportSize();
271
272
const columns = useMemo(() => {
273
if (width < 480) return 1;
274
if (width < 768) return 2;
275
if (width < 1024) return 3;
276
return 4;
277
}, [width]);
278
279
return (
280
<div
281
style={{
282
display: 'grid',
283
gridTemplateColumns: `repeat(${columns}, 1fr)`,
284
gap: '1rem'
285
}}
286
>
287
{children}
288
</div>
289
);
290
}
291
292
// Virtual keyboard handling on mobile
293
function MobileForm() {
294
const { height } = useViewportSize();
295
const [initialHeight] = useState(() => window.innerHeight);
296
297
// Detect virtual keyboard
298
const isVirtualKeyboardOpen = height < initialHeight * 0.8;
299
300
return (
301
<form className={isVirtualKeyboardOpen ? 'keyboard-open' : ''}>
302
<input placeholder="This adjusts for virtual keyboard" />
303
<button type="submit">Submit</button>
304
</form>
305
);
306
}
307
```
308
309
### Resize Observer
310
311
Hook for observing element resize with ResizeObserver API and fallbacks.
312
313
```typescript { .api }
314
/**
315
* Observes element resize with ResizeObserver API
316
* @param options - Configuration for resize observation
317
*/
318
function useResizeObserver<T extends Element>(options: {
319
ref: RefObject<T>;
320
box?: ResizeObserverBoxOptions;
321
onResize: () => void;
322
}): void;
323
324
type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box";
325
```
326
327
**Usage Examples:**
328
329
```typescript
330
import { useResizeObserver } from "@react-aria/utils";
331
332
function ResizableComponent() {
333
const elementRef = useRef<HTMLDivElement>(null);
334
const [size, setSize] = useState({ width: 0, height: 0 });
335
336
useResizeObserver({
337
ref: elementRef,
338
onResize: () => {
339
if (elementRef.current) {
340
const { offsetWidth, offsetHeight } = elementRef.current;
341
setSize({ width: offsetWidth, height: offsetHeight });
342
}
343
}
344
});
345
346
return (
347
<div ref={elementRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid #ccc' }}>
348
<p>Resize me!</p>
349
<p>Current size: {size.width} x {size.height}</p>
350
</div>
351
);
352
}
353
354
// Responsive text sizing based on container
355
function ResponsiveText({ children }) {
356
const containerRef = useRef<HTMLDivElement>(null);
357
const [fontSize, setFontSize] = useState(16);
358
359
useResizeObserver({
360
ref: containerRef,
361
onResize: () => {
362
if (containerRef.current) {
363
const width = containerRef.current.offsetWidth;
364
// Scale font size based on container width
365
const newFontSize = Math.max(12, Math.min(24, width / 20));
366
setFontSize(newFontSize);
367
}
368
}
369
});
370
371
return (
372
<div ref={containerRef} style={{ fontSize: `${fontSize}px` }}>
373
{children}
374
</div>
375
);
376
}
377
378
// Chart that redraws on resize
379
function Chart({ data }) {
380
const chartRef = useRef<HTMLCanvasElement>(null);
381
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
382
383
useResizeObserver({
384
ref: chartRef,
385
box: 'content-box',
386
onResize: () => {
387
if (chartRef.current) {
388
const { clientWidth, clientHeight } = chartRef.current;
389
setDimensions({ width: clientWidth, height: clientHeight });
390
}
391
}
392
});
393
394
// Redraw chart when dimensions change
395
useEffect(() => {
396
if (chartRef.current && dimensions.width > 0) {
397
drawChart(chartRef.current, data, dimensions);
398
}
399
}, [data, dimensions]);
400
401
return <canvas ref={chartRef} />;
402
}
403
```
404
405
## Advanced Layout Patterns
406
407
Complex scrolling and layout scenarios:
408
409
```typescript
410
import {
411
useViewportSize,
412
useResizeObserver,
413
scrollIntoView,
414
getScrollParents
415
} from "@react-aria/utils";
416
417
function InfiniteScrollList({ items, onLoadMore }) {
418
const containerRef = useRef<HTMLDivElement>(null);
419
const sentinelRef = useRef<HTMLDivElement>(null);
420
const { height: viewportHeight } = useViewportSize();
421
422
// Resize handling for container
423
useResizeObserver({
424
ref: containerRef,
425
onResize: () => {
426
// Recalculate visible items when container resizes
427
updateVisibleItems();
428
}
429
});
430
431
// Intersection observer for infinite scroll
432
useEffect(() => {
433
if (!sentinelRef.current) return;
434
435
const observer = new IntersectionObserver(
436
([entry]) => {
437
if (entry.isIntersecting) {
438
onLoadMore();
439
}
440
},
441
{ threshold: 0.1 }
442
);
443
444
observer.observe(sentinelRef.current);
445
return () => observer.disconnect();
446
}, [onLoadMore]);
447
448
// Smart scrolling for keyboard navigation
449
const scrollToItem = useCallback((index: number) => {
450
const container = containerRef.current;
451
const item = container?.children[index] as HTMLElement;
452
453
if (container && item) {
454
scrollIntoView(container, item);
455
}
456
}, []);
457
458
return (
459
<div
460
ref={containerRef}
461
style={{ height: Math.min(viewportHeight * 0.8, 600), overflow: 'auto' }}
462
>
463
{items.map((item, index) => (
464
<div key={item.id}>
465
{item.content}
466
</div>
467
))}
468
<div ref={sentinelRef} style={{ height: 1 }} />
469
</div>
470
);
471
}
472
```