Tutorial: Product Gallery
We will build a product gallery page together during this tutorial using Vue 3 and Lynx. This tutorial does not assume any existing Lynx knowledge. The techniques you'll learn in the tutorial are fundamental to building any Lynx pages and applications.
This tutorial is designed for people who prefer to learn by doing and want to quickly try making something tangible. If you prefer learning each concept step by step, start with Describing the UI.
What are we building?
A furniture gallery with a beautiful waterfall layout, tap-to-like interactions, auto-scrolling, and a custom scrollbar — driven by Main Thread Script for buttery-smooth performance. Each section builds incrementally on the previous one.
Setup for the tutorial
Check out the detailed quick start doc that will guide you through creating a new Lynx project. We recommend TypeScript for a better development experience, provided by static type checking and better editor IntelliSense.
You'll see lots of beautiful images throughout this guide. We've put together a package of sample images you can download here to use in your projects.
Your lynx.config.ts should look like this:
Adding Styles
Since the focus of this tutorial is not on how to style your UI, you may just save some time and directly copy the below gallery.css file:
gallery.css
and import it in your entry file:
This makes sure your UI looks great when you are following this tutorial.
Lynx supports a wide variety of styling features, including global styles, CSS Modules, inline styles, Sass, CSS variables, and more! Please refer to Rspeedy - Styling for how to pick your best styling configurations.
Your First Component: An Image Card
Now, let's start by creating the first image card, which will be the main part of this page.
Great, you can now see the image card displayed. Here, we use the <image> element to display your image. You only need to give it a width and height (or specify the aspectRatio property as shown here), and it will automatically resize to fit the specified dimensions. This component can receive a picture property via defineProps, allowing you to change the image it displays.
The src Attribute of Images
The Lynx <image> element can accept a local relative path as the src attribute to render an image. All images in this page are sourced locally, and these paths need to be imported before use. However, if your images are stored online, you can easily replace them with web image addresses.
Like standard Vue, Vue Lynx uses createApp(Component).mount(). The difference is that there is no DOM element to mount to — mount() is called with no arguments and attaches to the Lynx page root automatically.
Adding interactivity: Like an Image Card
We can add a small white heart in the upper right corner and make it the like button for the image card. Here, we implement a small component called LikeIcon:
We want each card to know whether it has been liked, so we added isLiked, which is its internal data. It can use this internal data to save your changes.
ref(false) creates a reactive variable. When isLiked.value changes, Vue's reactivity system automatically re-renders the component.
Then we add the @tap event to the wrapper <view>, so that when the user clicks the heart, it triggers this event and changes the state of isLiked:
What is "@tap"?
Vue Lynx uses the same @eventName syntax you know from Vue on the web, but Lynx has its own set of events — @tap instead of @click, @scroll instead of native scroll, etc. Learn more on the Event Handling page.
Finally, we use isLiked to control the like effect. Because isLiked is reactive, LikeIcon will respond to its changes, turning into a red heart icon. The <view> elements used to render the ripple animation are conditionally rendered with v-if:
We then compose LikeIcon with ImageCard into a LikeImageCard component (see the code in the viewer above).
To give the like a better visual interaction effect, we added CSS animations in gallery.css. You can also learn more about animations in the Animation section and replace them with your preferred style!
Displaying More Images with <list>
To show all your beautiful images, you may need help from <list>. This way, you will get a scrollable page that displays a large number of similar images:
Special child elements of list
Each child of <list> needs to be <list-item>, and you must specify a unique and non-repeating :key and :item-key attribute, otherwise it may not render correctly.
We use a waterfall layout as the child node layout option. <list> also accepts other layout types, which you can refer to in the <list> documentation.
You can refer to the Scrolling documentation to learn more about scrolling and scrolling elements.
Auto-Scrolling via Element Methods
If you want to create a desktop photo wall, you need to add an auto-scroll feature to this page. Your images will be slowly and automatically scrolled, allowing you to easily see more images:
We use onMounted combined with nextTick to call the autoScroll method after the list is rendered. We use lynx.createSelectorQuery() to select the list element by its custom-list-name attribute and invoke the native method.
What is "invoke"?
In Lynx, all native elements have a set of "methods" that can be called. Unlike on the web, this call is asynchronous, similar to message passing. You need to use invoke with the method name and parameters to call them.
onMounted + nextTick
onMounted runs once when the component is mounted. We call nextTick inside it to wait for the main thread to finish creating the native elements. In Vue Lynx, nextTick is enhanced to wait for the cross-thread ops flush — not just Vue's internal scheduler — so elements are guaranteed to exist when the callback fires.
How about a Custom Scrollbar?
Like most apps, we can add a scrollbar to this page to indicate how many images are left to be displayed. But we can do more! For example, we can replace the default progress bar of <list> with our preferred style:
The NiceScrollbar component exposes an adjustScrollbar method via defineExpose, which lets the parent component call methods on the child. Similar to the @tap event used to add the like functionality, we add the @scroll event to <list>, which will be triggered when the <list> element scrolls. To make the scrollbar respond faster to scroll events, we set scroll-event-throttle to 0.
list-item's estimated-main-axis-size-px
You may have noticed this attribute on <list-item>. It estimates the size of elements on the main axis when they are not yet rendered in <list>. This is very useful when we add a scrollbar, as we need to know how long the scrollbar needs to be to cover all elements.
We provide a utility method to estimate the size based on the current layout and image dimensions:
At this point, we have a complete page! But you may have noticed that the scrollbar we added still lags a bit during scrolling, not as responsive as it could be. This is because our adjustments are still happening on the background thread, not the main thread that responds to touch scrolling.
What are the background thread and main thread?
The biggest feature of Lynx is its dual-thread architecture. You can find a more detailed introduction in JavaScript Runtime.
A More Responsive Scrollbar
To optimize the performance of the scrollbar, we need to introduce Main Thread Script (MTS) to handle events on the main thread, migrating the adjustments we made in the previous step for the scrollbar's height and position from the background thread to the main thread.
To let you see the comparison more clearly, we keep both scrollbars:
The MTS scrollbar uses :main-thread-ref to bind a MainThreadRef to the element, allowing the Main Thread to directly manipulate it without any background thread round-trips.
Now you should be able to see that the scrollbar on the left (darkkhaki), controlled with main thread scripting, is smoother and more responsive compared to the scrollbar on the right that we implemented earlier. If you encounter issues in other UIs where updates need to happen immediately, try this method.
Key Vue concepts for MTS:
useMainThreadRef(null)— Creates a ref that can be resolved on the Main Thread:main-thread-bindscroll="ctx"— Binds a worklet handler to the scroll event (runs on Main Thread, zero thread crossings).toJSON()— Serializes the ref for cross-thread transfer (includes the internal_wvididentifier)
Wrapping Up
We remove the redundant scrollbar used for comparison, and our Gallery is now complete! Let's take a look at the final result:
Congratulations! You have successfully created a product gallery page! Throughout this tutorial, you've covered the basics of writing interactive UIs on the Lynx platform with Vue 3.
React vs Vue Quick Reference
Running the Examples
Then open each bundle in LynxExplorer: