Vue Features Compatibility

Vue Lynx is built on the official Vue 3 runtime core (@vue/runtime-core), so you should expect your Vue code to just work. The core rendering path — Composition API, SFCs, reactivity, template directives — works the same way you'd expect from standard Vue, with no Lynx-specific adaptation required.

Below is a feature-by-feature breakdown of the Vue features we've verified on Lynx. Where Lynx-specific caveats exist, they are called out inline. For details on how the dual-thread architecture works under the hood, see Understanding the Dual-Thread Model.

Reactivity + Composables

Vue Lynx reuses 100% of Vue's reactivity core (@vue/reactivity). Every reactivity API works identically to standard Vue, with no Lynx-specific caveats or adaptations.

The example below demonstrates reactive() + toRefs(), and a useStopwatch() composable that encapsulates reactive state:

SFC CSS Features

Plain <style> blocks, imported .css files, <style module>, <style scoped>, and v-bind() in CSS all work on Lynx.

Feature support details
FeatureStatus
<style> (plain)Works
Imported .css filesWorks
<style module>Works
v-bind() in <style>Works (requires config, see below)
<style scoped>Works (see caveats below)

:::warning <style scoped> caveats :deep(), :slotted(), and :global() are not yet supported (#164, #165).

v-bind() in <style> requires two config options so the Lynx engine recognizes CSS custom properties in inline styles and cascades them to descendants:

lynx.config.ts
pluginVueLynx({
  enableCSSInlineVariables: true,
  enableCSSInheritance: true,
})
Known limitation

Layout-affecting properties (e.g., font-size) driven by v-bind() apply correctly on initial render but may not update visually on reactive change. This is a Lynx engine limitation. Workaround: drive layout properties via :style binding directly on the element.

:::

v-model

Vue's v-model creates two-way bindings. On components, the child uses defineModel() (Vue 3.4+) to declare a model prop, and the parent binds it with v-model. On native <input> and <textarea> elements, v-model works just like standard Vue, including the .lazy, .trim, and .number modifiers.

The example demonstrates:

  1. Default modeldefineModel<number>() with v-model="count" for a counter
  2. Named modelsdefineModel('title') + defineModel('body') with v-model:title / v-model:body
  3. Native input v-modelv-model directly on <input>
Caveat

v-model on <select>, <input type="checkbox">, and <input type="radio"> is not supported — Lynx has no native equivalents for these elements. Use component-level v-model with custom components instead.

Event Modifiers

Vue's event modifiers (.once, .stop, .self) work on Lynx. .prevent is accepted as a compatibility no-op — see the table below for details.

Feature support details
ModifierStatusNotes
.onceWorksHandler fires at most once. The Vue compiler emits onTapOnce prop keys; withModifiers also supports it.
.stopWorksUses Lynx's native catchEvent mechanism to stop propagation at the element level. Also calls stopPropagation() in DOM/test environments.
.selfWorksCompares by uid (Lynx native) or uniqueId (web preview) since cross-thread event objects are always distinct references. Falls back to reference equality in DOM/test environments.
.preventCompatibility no-opAccepted silently so web code runs on Lynx without modification. Lynx has no browser default actions to cancel (no <a> navigation, no <form> submission), so this modifier has no observable effect.
Main Thread Script event handlers

Vue's modifier system does not apply to :main-thread-bind* handlers (e.g. :main-thread-bindtap). These use Lynx-native v-bind syntax and bypass Vue's v-on event pipeline entirely — the compiler never generates onTapOnce keys or withModifiers() wrappers for them. Use the native equivalents instead: :main-thread-catchtap for propagation stopping, and inline Main Thread Script logic for .once/.self behaviour.

The example below demonstrates .once, .stop, and .self. The .prevent card explains why no interactive demo is shown for that modifier:

Slots

Vue slots are the primary composition mechanism for passing template content into child components. Vue Lynx supports default slots, named slots, and scoped slots.

The example below demonstrates all three patterns:

  1. Default slot — content projected into a <Card> component
  2. Named slots#header and #footer with fallback content
  3. Scoped slot — a <DataList> exposes each item to the parent for custom rendering

Provide / Inject

Vue's provide and inject APIs let an ancestor component serve as a dependency injector for all its descendants, regardless of how deep the component hierarchy is. This avoids prop drilling through intermediate components.

The example below provides a reactive theme ref and a static appName string at the root. A grandchild component injects both — the middle layer passes nothing down.

Suspense

Vue's <Suspense> displays fallback content while waiting for async components to resolve. On Lynx, <Suspense> works with both async setup() (top-level await in <script setup>) and defineAsyncComponent for deferred loading.

Code-splitting

defineAsyncComponent(() => import('./X.vue')) emits webpack async chunks that need Lynx's lazy-bundle runtime (lynx.requireModuleAsync / lynx.loadLazyBundle). Those APIs are not available in Lynx Web Preview, so the docs example uses delayed promises over statically imported components instead — the Suspense fallback path is the same.

Transition

Vue's <Transition> and <TransitionGroup> components apply enter/leave animations when elements are inserted or removed.

Since #248 (v0.5), transitions are no longer experimental. Vue Lynx follows Vue's persisted transition behavior for v-show: enter and leave hooks run on every visibility change, and display: none is applied only after the leave animation finishes. This makes patterns such as drawers, sheets, popovers, and other state-preserving overlays behave consistently with official Vue on both Web and native Lynx.

Lynx-specific limitations

Always pass an explicit :duration prop — getComputedStyle() is unavailable from the background thread. Move (FLIP) animations in <TransitionGroup> are not supported since getBoundingClientRect() is unavailable. Enter and leave transitions are supported.

KeepAlive

Vue's <KeepAlive> caches inactive component instances instead of destroying them. When a component is toggled back in, its state is preserved. The include, exclude, and max props are all supported. The onActivated and onDeactivated lifecycle hooks fire as expected.

Options API

Vue 3 ships the Options API alongside the Composition API for backward compatibility. By default, Vue Lynx enables it (optionsApi: true in the plugin), but you can disable it to reduce bundle size:

lynx.config.ts
pluginVueLynx({
  optionsApi: false, // tree-shakes the Options API runtime (~9 kB)
})

The example below uses defineComponent with data(), computed, watch, methods, and the mounted lifecycle hook:

v-once / v-memo

v-once and v-memo work in Vue Lynx without any configuration. On Lynx a cache hit skips the whole cross-thread op batch for that subtree — prefer v-once for never-changing content, and v-memo when a subtree should update only for a short dependency list (common in v-for rows).

For render functions, Vue's withMemo helper (what the compiler emits for v-memo) is re-exported from vue-lynx.

Teleport

Vue's <Teleport> moves a template fragment to a different place in the UI tree. Vue Lynx supports it for to="#id" string selectors via a background-thread idRegistry lookup — the same pattern as a modal rendered at the page root while its trigger lives deeper in the component tree.

Caveats

Direct element refs and non-ID selectors (e.g. to=".class" or to="body") are not yet supported. Give the destination an id and target it with to="#that-id".

The target element must already exist when <Teleport> mounts — place the id host earlier in the tree than the <Teleport> (or outside the component that owns it). Vue cannot resolve a target that is created by the same render as the teleport.

The example below shows a modal teleported to #overlay-root, plus reactive content with the disabled prop:

The <page> Top-Level Node

Every Lynx screen has one native <page> root. Vue Lynx creates it automatically; write an explicit <page> only when you need to bind a class, style, event, or ref to the root:

<template>
  <page class="app" :style="pageStyle" @tap="onPageTap">
    <view>...</view>
  </page>
</template>

An explicit <page> compiles to a transparent built-in component that applies attributes to the existing native page — it does not create a second one. Use at most one per screen, as the outermost element. Prefer Lynx Engine 3.8.1+ when using an explicit <page>.

Unsupported Features

Some Vue built-in features are not yet adapted to the dual-thread native environment:

FeatureReasonAlternative
<Transition> auto-durationgetComputedStyle() is unavailable on the background threadAlways pass an explicit :duration prop
withKeys key modifiersNo-op on native — Lynx's native runtime converts keyboard input to named events (e.g. confirm) before reaching JS, so event.key is never populated on iOS/Android. Works on web preview (lynx-stack#2594). Native support requires an upstream runtime change.Native: use @confirm directly on <input>