Page Feature #
Document-level scrolling - use the browser's native scrollbar instead of container scrolling.
Overview #
The withPage() feature enables page-level scrolling where the list participates in the document flow instead of scrolling within a container. Perfect for blog posts, infinite scroll feeds, and full-page lists.
Import:
import { vlist, withPage } from '@floor/vlist';
Bundle cost: +0.9 KB gzipped
Quick Start #
import { vlist, withPage } from '@floor/vlist';
const feed = vlist({
container: '#feed',
items: articles,
item: {
height: 300,
template: (article) => `
<article>
<h2>${article.title}</h2>
<p>${article.excerpt}</p>
</article>
`,
},
})
.use(withPage())
.build();
The list now scrolls with the page, using the browser's native scrollbar.
Configuration #
No configuration needed:
withPage() // That's it!
The feature automatically:
- ✅ Changes scroll element from container to
window - ✅ Uses document scroll position
- ✅ Adjusts viewport calculations for page-level scrolling
- ✅ Handles window resize events
Use Cases #
Blog Posts / Articles #
import { vlist, withPage } from '@floor/vlist';
const blog = vlist({
container: '#articles',
items: posts,
item: {
height: 400, // Approximate article preview height
template: (post) => `
<article class="post">
<h1>${post.title}</h1>
<time>${post.date}</time>
<div class="content">${post.excerpt}</div>
<a href="/posts/${post.slug}">Read more →</a>
</article>
`,
},
})
.use(withPage())
.build();
Infinite Scroll Feed #
import { vlist, withPage, withAsync } from '@floor/vlist';
const feed = vlist({
container: '#feed',
item: {
height: 250,
template: (item) => {
if (!item) return `<div class="skeleton">Loading...</div>`;
return `<div class="card">${item.content}</div>`;
},
},
})
.use(withPage())
.use(withAsync({
adapter: {
read: async ({ offset, limit }) => {
const res = await fetch(`/api/feed?offset=${offset}&limit=${limit}`);
return res.json();
},
},
}))
.build();
Combines page scrolling with lazy loading for infinite scroll.
Product Listings #
import { vlist, withPage } from '@floor/vlist';
const products = vlist({
container: '#products',
items: productList,
item: {
height: 350,
template: (product) => `
<div class="product-card">
<img src="${product.image}" />
<h3>${product.name}</h3>
<p class="price">$${product.price}</p>
<button>Add to Cart</button>
</div>
`,
},
})
.use(withPage())
.build();
Search Results #
import { vlist, withPage } from '@floor/vlist';
const results = vlist({
container: '#results',
items: searchResults,
item: {
height: 120,
template: (result) => `
<div class="result">
<a href="${result.url}">${result.title}</a>
<p>${result.snippet}</p>
<cite>${result.domain}</cite>
</div>
`,
},
})
.use(withPage())
.build();
How It Works #
Without `withPage()` (Default) #
<div id="list" style="height: 600px; overflow: auto;">
<!-- List scrolls within this container -->
<!-- Container has scrollbar -->
</div>
Scrollbar appears on the container. List has fixed height.
With `withPage()` #
<div id="list" style="overflow: visible; height: auto;">
<!-- List participates in page flow -->
<!-- Document scrollbar controls scrolling -->
</div>
Scrollbar appears on the page. List height is dynamic based on content.
Technical Details #
The feature modifies:
- Scroll element:
windowinstead of container - Scroll position:
window.scrollYinstead ofcontainer.scrollTop - Viewport height:
window.innerHeightinstead ofcontainer.clientHeight - Position calculations: Relative to document, not container
- Resize handling: Listens to
windowresize instead of container resize
Compatibility #
✅ Compatible With #
withAsync()- Infinite scroll feedswithSelection()- Selectable full-page listswithScale()- Large page-level datasetsreverse: true- Reverse mode with page scroll
❌ Incompatible With #
withScrollbar()- Page uses native browser scrollbarorientation: 'horizontal'- Page scroll is vertical onlywithGrid()in some cases - Grid works but may need careful styling
Note: The builder will throw an error if you try to combine withPage() with withScrollbar().
Styling Considerations #
Container Styling #
With page scrolling, the container doesn't need a fixed height:
#feed {
/* No height or overflow needed */
width: 100%;
max-width: 800px;
margin: 0 auto;
}
Full-Height Items #
For full-viewport items (like slides), use viewport units:
const slides = vlist({
container: '#slides',
items: slideData,
item: {
height: () => window.innerHeight, // Full viewport height
template: (slide) => `<section class="slide">...</section>`,
},
})
.use(withPage())
.build();
// Re-calculate on resize
window.addEventListener('resize', () => {
slides.destroy();
// Recreate with new heights
});
Scroll Behavior #
The native page scrollbar provides:
- ✅ Familiar scroll behavior for users
- ✅ Browser-native momentum scrolling
- ✅ Scroll position in browser history
- ✅ No custom scrollbar styling needed
API #
The withPage() feature doesn't add any new methods. All standard VList methods work:
const list = vlist({ ... })
.use(withPage())
.build();
// All standard methods work
list.scrollToIndex(50);
list.getScrollPosition();
list.setItems(newArticles);
Events #
All standard events work with page scrolling:
list.on('scroll', ({ scrollPosition, direction }) => {
console.log('Page scrolled to:', scrollPosition);
});
list.on('range:change', ({ range }) => {
console.log('Visible items:', range);
});
list.on('item:click', ({ item, index }) => {
console.log('Clicked:', item);
});
Performance #
Benefits #
- ✅ Browser-optimized - Native scroll handling
- ✅ No custom scrollbar - One less thing to render
- ✅ Familiar UX - Users expect page scroll behavior
- ✅ History integration - Scroll position in browser history
Considerations #
- Browser scrollbar appearance varies by OS
- Less control over scroll behavior than container scrolling
- Can't combine with custom scrollbar styling
Examples #
See these interactive examples at vlist.dev/examples:
- Window Scroll - Infinite scroll with page-level scrolling
Best Practices #
When to Use `withPage()` #
✅ Use for:
- Blog posts and articles
- Infinite scroll feeds
- Product listings
- Search results
- Full-page content
✅ Don't use for:
- Contained lists within a page section (use default container scroll)
- When you need custom scrollbar styling (use
withScrollbar()) - Horizontal scrolling (page scroll is vertical only)
- Multiple lists on same page (only one can use page scroll)
Combining with Async Loading #
import { vlist, withPage, withAsync } from '@floor/vlist';
const feed = vlist({
container: '#feed',
item: {
height: 300,
template: renderPost,
},
})
.use(withPage())
.use(withAsync({
adapter: {
read: async ({ offset, limit }) => {
const res = await fetch(`/api/posts?offset=${offset}&limit=${limit}`);
return res.json();
},
},
loading: {
cancelThreshold: 5, // Skip loads when scrolling fast
},
}))
.build();
// Events
list.on('load:start', () => showLoadingIndicator());
list.on('load:end', () => hideLoadingIndicator());
SEO Considerations #
For better SEO with infinite scroll:
- Render initial items server-side
- Use
withPage()+withAsync()for progressive loading - Consider implementing "Load More" button as fallback
- Update page URL as user scrolls through content
Troubleshooting #
List doesn't scroll #
Problem: Container has height: 600px set in CSS
Solution: Remove fixed height from container
/* ❌ Don't */
#list { height: 600px; overflow: auto; }
/* ✅ Do */
#list { /* No height needed */ }
Scroll position jumps on data load #
Problem: Items have variable heights but you're using fixed height
Solution: Use variable height function or measure items first
Multiple lists on same page #
Problem: Only one list can use withPage()
Solution: Use page scroll for main list, container scroll for others
See Also #
- Features Overview - All available features
- Async Feature - Combine for infinite scroll
- Scrollbar Feature - Alternative: custom scrollbar
- Builder Pattern - How to compose features
- Examples - Interactive examples
Bundle cost: +0.9 KB gzipped
Priority: 10 (runs first - modifies scroll element)
Conflicts: Cannot combine with withScrollbar()