Guides

Vue Lynx

How Vue Lynx renders Vue components to native iOS, Android, and web through ByteDance's Lynx runtime, and what changes when you write Vue against it.

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

ElementWhat it doesWorth knowing
<view>Container, the <div> of LynxFlexbox 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 screenText 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 imagesNeeds 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 containerBuilds 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 recyclerChildren 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 nodeOne 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.

ElementWhat it doesWorth knowing
<input>Single-line text fieldBehind every form control in @vyui/kit, so a missing XElement dependency looks like the whole form vanishing.
<textarea>Multi-line text fieldSame dependency and the same failure mode.
<overlay>A layer detached from the document flow and promoted out of LynxMeant 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 graphicsParsed 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 onlyTakes 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 pagesVySwiper does this from <view> and main-thread gestures instead, which keeps the styling in your hands.
<scroll-coordinator>Coordinates nested scrolling containersFor the case where an inner scroller should take over once the outer one bottoms out.
<blur-view>Blur backdropReal native blur rather than a CSS filter, and priced accordingly.
<webview>Embedded web contentThe usual reason a Lynx app still carries a web engine.
<video>Video playbackNative player, driven by attributes rather than by a JavaScript API.
<title-bar-view>Native header barRelevant 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.

Vue Lynx ships under a pre-alpha label and Vy UI is alpha. Vy UI's component surface has been the steadier of the two; the pre-alpha runtime underneath is the reason I would not point a production launch here today.

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.

Lynx UI frameworks

The component libraries available for Lynx, compared.

Lynx vs React Native

How the two architectures differ and when each one makes sense.

Install Vy UI

Add the packages to a Vue Lynx project.