Mobile & Touch Support #
How vlist works on mobile devices, touch interactions, and optimization strategies for mobile browsers.
Overview #
vlist works excellently on mobile devices through native browser scrolling capabilities. The library relies on standard browser scroll behavior rather than custom touch handlers, providing a familiar, high-performance experience on all mobile platforms.
Quick Summary #
| Feature | Mobile Support | Notes |
|---|---|---|
| Touch scrolling | ✅ Native | Momentum, bounce, all standard gestures |
| Performance | ✅ Optimized | Virtual rendering, passive listeners, RAF throttling |
| Responsive layout | ✅ Automatic | Works at any viewport size |
| Custom scrollbar drag | ⚠️ Mouse only | Native scroll still works perfectly |
| Keyboard navigation | ❌ N/A | Not applicable on mobile |
| Browser support | ✅ Excellent | iOS Safari 12+, Chrome, Firefox, Edge |
Touch Scrolling #
Native Browser Scrolling #
vlist uses standard overflow: auto on the viewport element, allowing the browser to handle all touch interactions natively:
- Momentum scrolling — Swipe and release for inertial scrolling
- Bounce effect — Native overscroll behavior on iOS
- Pinch zoom — Standard pinch gestures (if not disabled)
- Pull-to-refresh — Native browser behavior (if implemented)
All scroll event listeners use { passive: true } for optimal performance:
// From scroll controller
viewport.addEventListener('scroll', onScrollFrame, { passive: true });
This ensures the browser can immediately handle touch events without waiting for JavaScript, resulting in silky-smooth 60fps scrolling.
Why Native Scrolling? #
Rather than implementing custom touch handlers (touchstart, touchmove, touchend), vlist delegates to the browser's highly-optimized native scrolling:
✅ Advantages:
- Better performance (hardware-accelerated)
- Platform-consistent behavior (iOS bounce, Android overscroll glow)
- Automatic handling of edge cases (interrupted gestures, multi-touch, etc.)
- Accessibility features (VoiceOver/TalkBack scrolling)
- Less code to maintain
❌ Custom touch handling would require:
- Manual velocity calculation
- Momentum simulation
- Platform-specific behavior replication
- Touch event coordination (prevent conflicts)
- Significantly more code and testing
Performance on Mobile #
vlist includes extensive optimizations specifically beneficial for mobile devices:
Virtual Rendering #
Only visible items are rendered in the DOM:
Total items: 1,000,000
Rendered items: ~20 (depending on viewport height)
DOM nodes: Minimal
This keeps memory usage low and garbage collection pressure minimal — crucial for mobile devices with limited RAM.
Passive Event Listeners #
All scroll listeners are passive, allowing the browser to scroll immediately:
{ passive: true } // Never blocks scrolling
RAF Throttling #
Scroll processing is throttled to one update per animation frame (60fps max):
requestAnimationFrame(() => {
updateVisibleItems();
});
CSS Containment #
Items use CSS contain for better rendering performance:
.vlist-items {
contain: layout style;
}
.vlist-item {
contain: content;
will-change: transform;
}
This tells the browser to optimize rendering by isolating item repaints.
Zero-Allocation Scroll Path #
The scroll hot path allocates no objects — no garbage collection during scroll:
// Reuses existing range object instead of creating new ones
range.start = newStart;
range.end = newEnd;
Element Pooling #
DOM elements are recycled via createElementPool(), reducing GC pressure:
// Elements are reused, not destroyed and recreated
const el = pool.get();
updateElement(el, newData);
Responsive Viewport #
Viewport Meta Tag #
All vlist demos include the standard mobile viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
This ensures proper scaling on mobile devices.
Container Sizing #
vlist adapts to any container size. Common mobile patterns:
/* Full-screen list */
.vlist {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* List within a page */
.vlist {
height: 100vh;
height: 100dvh; /* Dynamic viewport height (iOS Safari) */
}
/* List with header */
.vlist {
height: calc(100vh - 60px);
height: calc(100dvh - 60px);
}
Dynamic Viewport Height #
On iOS Safari, the viewport height changes when the address bar shows/hides. Use dynamic viewport units:
.vlist {
height: 100dvh; /* Dynamic - adjusts to visible area */
/* height: 100vh; ← Static - may cause layout shifts */
}
Custom Scrollbar Limitations #
The custom scrollbar (used in compressed mode for 1M+ items) has limited mobile support:
Mouse Events Only #
The scrollbar currently uses mouse events for drag interactions:
thumb.addEventListener('mousedown', handleThumbMouseDown);
document.addEventListener('mousemove', handleThumbMouseMove);
document.addEventListener('mouseup', handleThumbMouseUp);
This means:
- ⚠️ Dragging the scrollbar thumb does not work on touch devices
- ✅ Native content scrolling still works perfectly
- ✅ The scrollbar appears and auto-hides correctly
- ⚠️ Hover-based scrollbar visibility does not work (no hover state on touch)
Workaround for Mobile #
Configure the scrollbar for mobile-friendly behavior:
import { vlist } from 'vlist';
import { withScrollbar } from 'vlist';
const list = vlist({ /* ... */ })
.use(withScrollbar({
autoHide: true, // Auto-hide after scroll stops
autoHideDelay: 800, // Shorter delay for mobile (default: 1000)
showOnHover: false, // No hover on touch devices
showOnViewportEnter: false // No mouse enter on touch devices
}))
.build();
Detection-Based Configuration #
Detect touch capability and adjust config:
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const scrollbarConfig = isTouchDevice
? {
autoHide: true,
autoHideDelay: 800,
showOnHover: false,
showOnViewportEnter: false
}
: {
autoHide: true,
autoHideDelay: 1000,
showOnHover: true,
showOnViewportEnter: true
};
const list = vlist({ /* ... */ })
.use(withScrollbar(scrollbarConfig))
.build();
Mobile-Optimized Configuration #
Recommended Settings #
import { vlist } from 'vlist';
const list = vlist({
container: '#list',
item: {
height: 60, // Taller items are easier to tap
template: (item) => `
<div style="padding: 16px; min-height: 44px;">
<!-- Minimum 44px tap target (iOS guidelines) -->
${item.name}
</div>
`
},
items: data,
scroll: {
wheel: true, // Still useful for desktop/trackpads
smoothing: false // Native momentum is better on mobile
}
})
.build();
Touch Target Sizing #
Follow platform guidelines for minimum tap target sizes:
| Platform | Minimum Size | Recommended |
|---|---|---|
| iOS | 44×44 px | 48×48 px |
| Android | 48×48 dp | 48×48 dp |
| Web (WCAG) | 44×44 px | 48×48 px |
.vlist-item {
min-height: 48px;
padding: 12px 16px;
}
Disable Text Selection During Scroll #
vlist includes user-select: none on items to prevent text selection during scrolling:
.vlist-item {
user-select: none;
}
This prevents accidental text selection when swiping through the list.
Mobile Browser Support #
Supported Browsers #
From the main README, vlist supports:
| Browser | Version | Mobile | Notes |
|---|---|---|---|
| Chrome | 60+ | ✅ Android | Excellent support |
| Safari | 12+ | ✅ iOS | Includes iOS Safari |
| Firefox | 55+ | ✅ Android | Full support |
| Edge | 79+ | ✅ Android | Chromium-based |
iOS-Specific Considerations #
Dynamic Viewport Height:
.vlist {
height: 100dvh; /* Adjusts when Safari UI shows/hides */
}
Safe Area Insets:
.vlist {
padding-bottom: env(safe-area-inset-bottom); /* iPhone notch */
}
Momentum Scrolling (Legacy):
.vlist-viewport {
-webkit-overflow-scrolling: touch; /* iOS < 13 */
}
Note: -webkit-overflow-scrolling: touch is deprecated in iOS 13+ but harmless to include for older devices.
Android-Specific Considerations #
Overscroll Glow: Native Android overscroll glow is preserved (can't be disabled in modern browsers).
Chrome Address Bar: Use dynamic viewport units to account for address bar hide/show:
height: 100dvh;
Testing on Mobile #
Device Testing #
Real Device Testing:
- Open vlist demo on your phone
- Test scrolling through large lists (10K, 100K, 1M items)
- Verify smooth 60fps scrolling
- Check memory usage doesn't grow during scroll
- Test rapid flicking (momentum scrolling)
Remote Debugging:
- iOS Safari: Use Safari on Mac → Develop → [Your iPhone]
- Android Chrome: Use
chrome://inspecton desktop Chrome
Performance Profiling #
Chrome DevTools (Android):
1. Connect device via USB
2. Enable USB debugging
3. chrome://inspect → Inspect device
4. Performance tab → Record while scrolling
Safari Web Inspector (iOS):
1. Connect device via USB
2. Enable Web Inspector on device
3. Safari → Develop → [Device] → [Page]
4. Timelines tab → Record
Lighthouse Mobile Audit #
Run Lighthouse in mobile mode:
npx lighthouse https://your-vlist-app.com \
--only-categories=performance \
--form-factor=mobile \
--screenEmulation.mobile=true
Known Limitations #
1. Custom Scrollbar Drag #
Issue: Cannot drag the custom scrollbar on touch devices.
Why: Uses mouse events (mousedown, mousemove, mouseup).
Impact: Low — users scroll by swiping content, not dragging scrollbars.
Workaround: Native scroll works perfectly. Configure scrollbar with showOnHover: false.
2. Hover States #
Issue: CSS :hover and JavaScript hover events don't work on touch.
Why: Touch devices have no persistent hover state.
Impact: Minimal — items can use :active for tap feedback.
Workaround: Use :active state for touch feedback:
.vlist-item:active {
background-color: var(--vlist-bg-hover);
}
3. Keyboard Navigation #
Issue: Arrow keys, Home, End not available on virtual keyboards.
Why: Mobile keyboards don't have these keys.
Impact: None — touch scrolling replaces keyboard navigation.
Future Enhancements #
Potential improvements for mobile support:
1. Touch Events for Custom Scrollbar #
Add touch event handlers to enable scrollbar drag on mobile:
thumb.addEventListener('touchstart', handleThumbTouchStart, { passive: false });
thumb.addEventListener('touchmove', handleThumbTouchMove, { passive: false });
thumb.addEventListener('touchend', handleThumbTouchEnd);
2. Pull-to-Refresh Support #
Optional feature for pull-to-refresh on mobile:
.use(withPullToRefresh({
onRefresh: async () => {
await loadNewItems();
}
}))
3. Swipe Actions #
Swipe-to-delete or swipe-to-reveal actions (iOS Mail style):
.use(withSwipeActions({
left: [{ label: 'Delete', action: (item) => { /* ... */ } }],
right: [{ label: 'Archive', action: (item) => { /* ... */ } }]
}))
4. Haptic Feedback #
Vibration feedback on selection or actions:
if ('vibrate' in navigator) {
navigator.vibrate(10); // 10ms haptic feedback
}
Examples #
Full-Screen Mobile List #
import { vlist } from 'vlist';
const list = vlist({
container: '#app',
item: {
height: 60,
template: (user) => `
<div class="user-item">
<img src="${user.avatar}" alt="${user.name}">
<div class="user-info">
<div class="user-name">${user.name}</div>
<div class="user-email">${user.email}</div>
</div>
</div>
`
},
items: users
})
.build();
/* Full-screen mobile layout */
#app {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.vlist {
height: 100dvh; /* Dynamic viewport height */
padding-bottom: env(safe-area-inset-bottom); /* iPhone notch */
}
/* Touch-friendly tap targets */
.user-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
min-height: 48px; /* Minimum tap target */
}
/* Touch feedback */
.vlist-item:active {
background-color: rgba(0, 0, 0, 0.05);
}
Chat UI (Reverse Mode) #
import { vlist } from 'vlist';
const chat = vlist({
container: '#chat',
reverse: true, // Start at bottom (newest messages)
item: {
height: (index) => messages[index].height,
template: (message) => `
<div class="message ${message.own ? 'own' : 'other'}">
${message.text}
</div>
`
},
items: messages
})
.build();
/* Mobile chat layout */
#chat {
height: calc(100dvh - 60px); /* Minus input bar */
padding-bottom: env(safe-area-inset-bottom);
}
.message {
max-width: 80%;
padding: 8px 12px;
margin: 4px 8px;
border-radius: 16px;
min-height: 44px; /* Tap target for long-press actions */
}
.message.own {
margin-left: auto;
background: #007aff;
color: white;
}
.message.other {
margin-right: auto;
background: #e5e5ea;
color: black;
}
Summary #
vlist provides excellent mobile support through native browser scrolling:
✅ Native touch scrolling — All standard gestures work perfectly
✅ High performance — Virtual rendering, passive listeners, optimized rendering
✅ Responsive — Works at any viewport size
✅ Cross-browser — iOS Safari 12+, Android Chrome, Firefox, Edge
⚠️ Minor limitation — Custom scrollbar drag requires mouse (but native scroll works)
The library's reliance on native scrolling rather than custom touch handling results in better performance, platform consistency, and less code complexity — making it an excellent choice for mobile applications with large lists.
Related Documentation #
- Accessibility — Screen reader support, ARIA attributes
- Performance — Optimization strategies and benchmarks
- Scroll — Scroll controller and configuration
Need help? Open an issue on GitHub or join the discussion.