scroll-view vs list
Lynx does not scroll arbitrary nodes the way the Web does. When content overflows a viewport you pick an explicit scroll container — almost always <scroll-view> or <list>. This guide explains how Lynx documents the difference, how that differs from Web Vue, and the Vue Lynx patterns that fall out of those choices.
For the platform reference, see Managing Scrolling on lynxjs.org.
Lynx vs Web
On the Web, almost any element can become a scrollport:
On Lynx, a plain <view> does not gain scrolling from overflow: scroll / overflow: auto. Only dedicated containers such as <scroll-view> and <list> scroll. That is the first design shift when you move a Vue app from DOM to Lynx: scrolling becomes a structural decision in the template, not a CSS property you sprinkle later.
Vue Lynx keeps Composition API, v-for, and SFCs — but your scroll trees look different from a typical Nuxt / Vite SPA.
What lynxjs.org says
Lynx's scrolling guide draws a clean line between the two containers:
<scroll-view>for basic scrolling — a fixed viewport; when children exceed it, setscroll-orientationtoverticalorhorizontal.<list>for large / infinite data — on-demand creation of visible items only.<list>for complex layouts —scroll-viewis linear only;listaddssingle,flow, andwaterfall.
The <scroll-view> API also warns:
- Every child is created up front (can hurt first paint).
- There is no reuse; too much content can exhaust memory.
- Prefer
<list>once content exceeds roughly three screens, or fake recycling with exposure events.
Think of <scroll-view> as “a short page that happens to scroll,” and <list> as “a recycling feed.”
Choose with a table
Rule of thumb from Lynx: under ~3 screens → scroll-view; beyond that → list.
<scroll-view> in Vue
Use ordinary Vue children — v-for, nested SFCs, sticky siblings. Nothing special is required beyond wrapping them in <scroll-view>.
Good fits: settings screens, forms, article bodies, short result pages (see also the Vue Query and TodoMVC examples).
Direct children of <scroll-view> only support linear / sticky layout. For richer CSS inside the scrollport, wrap content in a single child <view> and style that subtree — as recommended in the scroll-view docs.
<list> in Vue
<list> expects <list-item> children. Each item needs:
- Vue's
:key— reconciliation identity for the VNode tree - Lynx's
:item-key— identity for the native recycler (keep them equal)
Missing or colliding keys is a common cause of blank / wrong cells.
estimated-main-axis-size-px helps the engine size the scrollbar and jump before a cell has been measured — the gallery tutorial relies on the same hint for waterfall images.
Waterfall / flow layouts
This is the other half of the lynxjs.org distinction: scroll-view cannot do multi-column masonry; list can.
Each list-item can host a full Vue SFC — the gallery tutorial's LikeImageCard is the same pattern at product scale.
Vue design patterns that fall out of this
Moving from Web Vue to Vue Lynx does not change reactivity — it changes where you put scrolling and who owns recycling.
1. Explicit containers in the template
Stop reaching for overflow: auto on a root <view>. Decide up front:
That structural choice is the Lynx equivalent of picking a layout library on the Web.
2. Dual identity: :key + :item-key
Web v-for only needs :key. Lynx lists need both. Treat them as one stable business id:
The Elk port follows exactly this contract for Mastodon statuses.
3. Composable owns pages; <list> owns recycling
On the Web, infinite feeds usually mean:
useInfiniteQuery / custom composable + a virtualizer + an intersection sentinel.
On Lynx, drop the virtualizer. A composable appends to a ref array; @scrolltolower (and lower-threshold-item-count) asks for the next page. Native recycling keeps memory flat.
This is the same shape as Elk's TimelinePaginator: masto.js pagination in a composable, <list> for the viewport.
Vue Lynx's Main Thread list adapter flushes insertAction / removeAction / updateAction by diffing the last-flushed snapshot against the live listItems array (LIS move detection, same idea as ReactLynx's remove+insert moves). Covers append, prepend, mid-list insert, remove, same-list reorder, and platform-info updates. Still open: framework-side cell recycling and refreshing __UpdateListCallbacks each flush — #302, #303.
4. Prefer list over third-party virtual scrollers
Libraries that measure DOM nodes (getBoundingClientRect, absolute positioning of windows) do not map cleanly onto Lynx's dual-thread + native elements model. Prefer the built-in recycler unless you have a rare layout <list> cannot express.
5. Keep cells as Vue components
Recycling is a native concern; composition stays a Vue concern. Put presentational SFCs inside list-item — do not flatten everything into one mega-template just because the parent is a list.
Mutation demos
Prepend
Tap Prepend once. The TOP list cell must turn red (NEW …). Append lands at the bottom.
Reorder
Four full-bleed colors. The top strip is plain <view>s (Vue truth). The <list> below must show the same order after Yellow → top or Reverse.
Remove + filter
Remove: delete rows, then Reset same keys — must not toast duplicated item-key (2202). Filter toggle is a lighter regression tap.
Still open
- Framework-side cell recycling (
enqueueComponent/ recycle pool) — #302 - Refreshing
__UpdateListCallbackson every list flush — #303
Automated coverage: packages/testing-library → native list element · mutations.
Decision checklist
- Is the page mostly one linear document under ~3 screens? →
<scroll-view> - Are you rendering dozens / hundreds of similar rows? →
<list> - Do you need waterfall or multi-column flow? →
<list list-type="…"> - Will the dataset grow without bound? →
<list>+ composable +@scrolltolower - Did you set matching
:key/:item-key? → required for correct recycling - Prepend / reorder / remove? → supported via
update-list-infodiffs
See also
- Managing Scrolling (Lynx)
<scroll-view>API<list>API- Tutorial: Product Gallery — waterfall
<list>in a full app - Elk (Mastodon Client) — replacing Virtua with native
<list> - Main Thread Script — scroll-linked worklets on
<scroll-view>/<list>