Vue Lynx
Vue Lynx is a Vue 3 custom renderer that targets Lynx, the cross-platform framework ByteDance open-sourced in March 2025 and uses in parts of TikTok. Your components stay Vue with <script setup>, the Composition API, and ordinary reactivity, and they render to native views on iOS and Android instead of to the DOM. The same source runs on the web through Lynx's browser runtime.
Vue Lynx reuses @vue/runtime-core rather than reimplementing Vue, so reactivity, lifecycle, provide/inject, slots, and the component model are the same code running under a different renderer.
What changes
Four differences show up in the first hour, and none touch the component model.
The elements are Lynx elements. There is no <div> and no <span>. Layout goes in <view>, text has to sit inside a <text> element rather than loose in a container, images use <image>, scrolling uses <scroll-view>, and long lists use <list>, which is a native recycler rather than a rendered array. The full set is below, and it is short enough to read in one sitting.
Your code runs on a background thread. The main thread does nothing but render and handle high-priority input, and your JavaScript runs beside it. A Lynx app can paint its first frame before your component code finishes executing, and a gesture that tracks a finger at 60fps has to run in a main-thread worklet rather than a normal event handler. Most components never touch this, and drag-driven ones always do.
Browser globals are gone. There is no window and no document, so any library reaching for them needs adapting or replacing. That rules out most of the existing Vue component ecosystem, which is why a Lynx-specific component library has to exist.
Styling is real CSS, with limits. Lynx parses actual stylesheets rather than style objects, so selectors, custom properties, media queries, and transitions all work. The native engine resolves only one level of var() indirection, and a few properties behave differently from the browser.
The element set
Lynx documents eighteen elements, and the division that matters in practice is whether the host application links XElement, because those elements come from a separate native dependency and render as nothing when it is missing rather than failing loudly.
Core built-ins
| Element | What it does | Worth knowing |
|---|---|---|
<view> | Container, the <div> of Lynx | Flexbox by default. Android flattens views into draw commands, so gesture interception through pan-intercept-direction or native-interaction-enabled needs flatten set to false. Empty slots and v-if branches still realize as zero-size children, which gap-* then counts as real ones. |
<text> | Every string on screen | Text cannot sit loose in a <view>. It is an inline formatting context, so display does nothing, and color and font-family inherit into a nested <text> even with CSS inheritance off. text-maxline only clips with overflow: hidden, and selection needs both text-selection and flatten false. |
<image> | Raster images | Needs an explicit non-zero width and height, or auto-size. bindload and binderror are the only load signals, mode chooses the fit, and defer-src-invalidation holds the old frame so a changing src does not flash. |
<scroll-view> | Scrolling container | Builds every child up front with no recycling, and the docs put the switch to <list> at roughly three screens of content. Direct children only support linear and sticky layout, and sticky ones on Android need flatten false. |
<list> | Virtualized recycler | Children must be <list-item> carrying an item-key, and the list itself needs explicit dimensions since content does not size it. list-type picks single, flow, or waterfall, the latter two wanting span-count of 2 or more. Nodes are reused as they scroll, so per-item mutable state leaks between rows. |
<page> | The root node | One per page, generated for you unless you author it. Its width, height, left, and top cannot be set, because the host decides them. Style it through page or :root, and reach it from JavaScript with selectRoot(), which is what to measure a viewport against rather than the screen. |
<frame> | Embeds another Lynx page, like an <iframe> | Passes data and global-props into the embedded page and reports back through bindload and bindloadmetrics. |
XElement
These ship in the XElement pod on iOS and its Android equivalent, which Sparkling includes in its native shells. Leaving it out is a common first-run failure when building a custom host, since the components render empty and nothing is logged.
| Element | What it does | Worth knowing |
|---|---|---|
<input> | Single-line text field | Behind every form control in @vyui/kit, so a missing XElement dependency looks like the whole form vanishing. |
<textarea> | Multi-line text field | Same dependency and the same failure mode. |
<overlay> | A layer detached from the document flow and promoted out of Lynx | Meant for Lynx embedded inside a native page. Lynx's own docs say not to use it in an all-Lynx page, where position: fixed is the answer, which is what Vy UI's overlay root does. It must set position: fixed itself and take exactly one child. |
<svg> | Vector graphics | Parsed off the main thread and drawn as a single native view, covering 17 SVG tags and about 40 attributes. Masks are not among them, so mask-based icon sets render as solid boxes, and color has to be baked in rather than inherited from text-*. |
<refresh> | Pull-to-refresh, vertical only | Takes a <refresh-header> plus one scrollable child. It is registered as the wrapper <list> uses rather than as a standalone element, and putting a refresh-header under a <scroll-view> crashes iOS with LynxCreateUIException: refresh-header ui not found, which is why VyFeedList drives its own pull-to-refresh from touch worklets. |
<viewpager> | Swipeable full-width pages | VySwiper does this from <view> and main-thread gestures instead, which keeps the styling in your hands. |
<scroll-coordinator> | Coordinates nested scrolling containers | For the case where an inner scroller should take over once the outer one bottoms out. |
<blur-view> | Blur backdrop | Real native blur rather than a CSS filter, and priced accordingly. |
<webview> | Embedded web content | The usual reason a Lynx app still carries a web engine. |
<video> | Video playback | Native player, driven by attributes rather than by a JavaScript API. |
<title-bar-view> | Native header bar | Relevant when the host owns the navigation bar, as a Sparkling container does. |
Vy UI is built almost entirely out of the first two rows, with <input> and <textarea> under the form controls, <list> under VyFeedList, and <svg> under the icon layer. Everything else in the catalogue is composition on top of <view>.
Vue Lynx and ReactLynx
ReactLynx came first and remains the reference frontend, so the documentation, examples, and the official @lynx-js/lynx-ui library all assume React. Lynx itself is framework-agnostic by design, and the Lynx team has said non-React frameworks already account for roughly half of its usage internally.
Vue Lynx's constraint is ecosystem age. Its API surface still moves, and ReactLynx libraries cannot be consumed from Vue because they are React components. I would take ReactLynx for the most-supported path today, and Vue Lynx when your team writes Vue and a rewrite is not on the table.
Where components come from
Lynx gives you layout and text primitives, so dialogs, sheets, selects, toasts, sliders, and every gesture surface are application code unless a library provides them.
Vy UI is that library, in two layers. @vyui/core ships 45 headless primitives handling state, focus, keyboard, and gesture physics while leaving styling to you, and @vyui/kit ships 48 styled components on top of them, themed through Tailwind Variants. Drop from kit to core wherever the styled version stops fitting.
The Lynx UI frameworks comparison maps this against the ReactLynx options.
Starting a project
Install the packages into an existing Vue Lynx application:
npm i @vyui/core @vyui/kit
A styled component then works like any other Vue component:
<script setup>
import { VyButton } from '@vyui/kit'
</script>
<template>
<view>
<VyButton>Continue</VyButton>
</view>
</template>
Two pieces of setup are specific to Lynx, and Installation covers both. The bundler needs includeWorkletPackages so main-thread gesture code is registered, and Tailwind needs the Vy UI preset for the semantic tokens behind theming and dark mode.
What to expect on each target
Native iOS and Android are the targets Lynx was designed for, so accessibility works through the platform screen readers without extra wiring, gestures run on the main thread, and animations are native.
Web is a real target rather than a preview, and the component previews throughout these docs are the web runtime running real component code. It is not at parity. Lynx dispatches raw mouse events without synthesizing touch from them, so drag surfaces need explicit mouse handling, which Vy UI ships for every gesture component. Keyboard navigation is not wired, and the accessibility-* properties that native screen readers consume are ignored by browsers.
Those gaps are specific rather than uniform, so check the components you depend on against your targets.
Related
Lynx UI frameworks
A comparison of the UI component libraries available for Lynx today, covering what each one ships, which framework it targets, and how mature it is.
Lynx vs React Native
How Lynx and React Native differ in threading model, styling, framework support, and ecosystem maturity, and which one fits a given project.