or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation-and-transitions.mdevent-management.mdfocus-and-accessibility.mdid-and-refs.mdindex.mdlinks-and-navigation.mdmiscellaneous-utilities.mdplatform-detection.mdprops-and-events.mdscrolling-and-layout.mdshadow-dom-support.mdstate-and-effects.mdvirtual-events-and-input.md

scrolling-and-layout.mddocs/

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

```