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
:::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:
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:
- Default model —
defineModel<number>()withv-model="count"for a counter - Named models —
defineModel('title')+defineModel('body')withv-model:title/v-model:body - Native input v-model —
v-modeldirectly on<input>
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
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 worklet 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:
- Default slot — content projected into a
<Card>component - Named slots —
#headerand#footerwith fallback content - 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 lazy-loading.
Transition
Vue's <Transition> and <TransitionGroup> components apply enter/leave animations when elements are inserted or removed.
<Transition> and <TransitionGroup> are experimental. 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.
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:
The example below uses defineComponent with data(), computed, watch, methods, and the mounted lifecycle hook:
v-once
v-once renders an element or component exactly once and skips all future updates. It works in Vue Lynx without any configuration — the SFC template compiler emits a cache lookup using setBlockTracking and the component's _cache array, and the runtime short-circuits on subsequent renders.
In Lynx, v-once is more impactful than in the browser because a cache hit eliminates the entire cross-thread op batch. After the initial mount:
- The VNode patcher receives the same cached VNode object reference.
- No
patchPropcalls are made, so no ops enter the buffer. doFlushsees an empty buffer and skipscallLepusMethodentirely — the main thread is never contacted for that subtree.
Use v-once for genuinely static content that should never update after first render:
v-onceis a stronger guarantee thanv-memo— it caches unconditionally, with no dependency array to check. Prefer it when content is truly static.
v-memo
v-memo skips a subtree re-render when its dependency array hasn't changed. It works in Vue Lynx without any configuration — the SFC template compiler already emits the correct withMemo() calls, and the runtime bails out before the patcher runs.
In Lynx, v-memo is more impactful than in the browser because a cache hit eliminates the entire cross-thread op batch, not just DOM diffing. When deps are unchanged:
- The VNode patcher short-circuits (same VNode object reference).
- No
patchPropcalls are made, so no ops enter the buffer. doFlushsees an empty buffer and skipscallLepusMethodentirely — the main thread is never contacted for that subtree.
The primary use case is v-for lists where only some items change on each update:
For render functions written in JavaScript, withMemo is exported directly from vue-lynx:
Teleport
<Teleport> is supported for to="#id" string selectors. Direct element refs and non-ID selectors are not yet supported.
Unsupported Features
Some Vue built-in features are not yet adapted to the dual-thread native environment: