Vue-Lynx: Native Performance with the Simplicity of Vue
Introduction
In recent days, the pre-alpha of Vue-Lynx was announced by Xuan Huang, marking an important step toward bringing the Vue ecosystem into the world of native mobile development. Notably, Xuan Huang achieved this milestone with the support of AI, highlighting how artificial intelligence is increasingly accelerating the development of new technologies.
Lynx is an open-source technology developed by ByteDance with the goal of enabling developers to build truly native applications using web technologies. The idea is to deliver high performance and a smooth user experience, overcoming the typical limitations of hybrid solutions.
One of Lynx’s most interesting aspects is its framework-agnostic approach: it is not tied to a specific library like React or Vue.js, but is designed to integrate with different frameworks, giving developers full freedom in choosing their tools.
In this context, Vue-Lynx represents the first concrete attempt to integrate Lynx with Vue, opening up new possibilities for cross-platform development.
Get micbianchi97’s stories in your inbox
Join Medium for free to get updates from this writer.
In the next step, we’ll see how to use this technology by building a simple todo list with Vue-Lynx.
Project Setup
To create a new project and initialize it, run the following commands:
npm create vue-lynx@latest
cd <your-project-name>
npm install
npm run devOnce the project is running, you can replace the contents of App.vue and App.css with the example code.
App.vue
<script setup lang="ts">
import { computed, ref } from 'vue-lynx'
import './App.css'
type Todo = {
id: string
text: string
completed: boolean
createdAt: number
}
const todos = ref<Todo[]>([
{ id: 't-1', text: 'Learn Vue Lynx', completed: false, createdAt: Date.now() },
{ id: 't-2', text: 'Build a todo list', completed: true, createdAt: Date.now() },
])
const newTodoText = ref('')
const remainingCount = computed(() => todos.value.filter((t) => !t.completed).length)
const completedCount = computed(() => todos.value.filter((t) => t.completed).length)
/**
* Handles `<input>` changes (native input `v-model` is not supported).
*/
function handleTodoInput(e: unknown) {
const detail = (e as { detail?: { value?: unknown } }).detail
const value = detail?.value
if (typeof value !== 'string') return
newTodoText.value = value
}
/**
* Adds a new todo from `newTodoText`.
*/
function handleAddTodo() {
const text = newTodoText.value.trim()
if (!text) return
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
todos.value = [
...todos.value,
{ id, text, completed: false, createdAt: Date.now() },
]
newTodoText.value = ''
}
/**
* Toggles the completion status of a todo by id.
*/
function handleToggleTodo(id: string) {
todos.value = todos.value.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t,
)
}
/**
* Deletes a todo by id.
*/
function handleDeleteTodo(id: string) {
todos.value = todos.value.filter((t) => t.id !== id)
}
/**
* Clears all completed todos.
*/
function handleClearCompleted() {
if (completedCount.value === 0) return
todos.value = todos.value.filter((t) => !t.completed)
}
</script>
<template>
<view class="todo-app">
<view class="todo-header-row">
<text class="todo-header">Todo list</text>
</view>
<view class="todo-composer">
<input
class="todo-input"
:value="newTodoText"
@input="handleTodoInput"
placeholder="Write a new task"
/>
<view class="todo-add-button" :bindtap="handleAddTodo">
<text class="todo-add-button-text">Add</text>
</view>
</view>
<text class="todo-meta">
{{ remainingCount }} to do
<text class="todo-meta-dim"> · {{ completedCount }} completed</text>
</text>
<text v-if="todos.length === 0" class="todo-empty">No tasks. Add one above.</text>
<view v-else class="todo-list">
<view
v-for="todo in todos"
:key="todo.id"
class="todo-item"
:class="{ 'todo-item--completed': todo.completed }"
>
<view class="todo-check" :bindtap="() => handleToggleTodo(todo.id)">
<text class="todo-check-icon">{{ todo.completed ? '✓' : '' }}</text>
</view>
<text
class="todo-text"
:style="{
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? 'rgba(255, 255, 255, 0.6)' : '#fff',
}"
>
{{ todo.text }}
</text>
<view class="todo-delete-button" :bindtap="() => handleDeleteTodo(todo.id)">
<text class="todo-delete-text">Delete</text>
</view>
</view>
</view>
<view v-if="completedCount > 0" class="todo-footer">
<view class="todo-clear-completed" :bindtap="handleClearCompleted">
<text class="todo-clear-completed-text">Remove completed</text>
</view>
</view>
</view>
</template>App.css
:root { background-color: #000; --w: 720rpx; --txt: #fff; --s1: rgba(255, 255, 255, 0.06); --s2: rgba(255, 255, 255, 0.08); --b1: rgba(255, 255, 255, 0.12); --b2: rgba(255, 255, 255, 0.18); }
text { color: var(--txt); }
.todo-app {
min-height: 100vh;
width: 100%;
box-sizing: border-box;
padding: 90px 26rpx 26rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 18rpx;
}
.todo-header-row, .todo-composer, .todo-meta, .todo-empty, .todo-list, .todo-footer { width: 100%; max-width: var(--w); }
.todo-header-row, .todo-composer, .todo-item, .todo-add-button, .todo-delete-button, .todo-clear-completed, .todo-check { display: flex; flex-direction: row; align-items: center; box-sizing: border-box; }
.todo-add-button, .todo-delete-button, .todo-clear-completed, .todo-check { justify-content: center; }
.todo-add-button-text, .todo-delete-text, .todo-clear-completed-text { font-weight: 700; }
.todo-header { font-size: 40rpx; font-weight: 800; margin-bottom: 12rpx; }
.todo-header-row { justify-content: space-between; gap: 16rpx; }
.todo-composer { gap: 16rpx; }
.todo-input { flex: 1; height: 72rpx; padding: 0 20rpx; border-radius: 16rpx; background: var(--s2); border: 1px solid var(--b2); color: #fff; font-size: 26rpx; }
.todo-add-button { height: 72rpx; padding: 0 22rpx; border-radius: 16rpx; background: #1565c0; border: 1px solid var(--b1); }
.todo-meta { font-size: 24rpx; color: rgba(255, 255, 255, 0.8); }
.todo-meta-dim { color: rgba(255, 255, 255, 0.6); }
.todo-empty { font-size: 24rpx; color: rgba(255, 255, 255, 0.7); padding: 26rpx 0; }
.todo-list { display: flex; flex-direction: column; gap: 14rpx; }
.todo-item { width: 100%; gap: 16rpx; padding: 18rpx; border-radius: 18rpx; background: var(--s1); border: 1px solid var(--b1); }
.todo-check { width: 44rpx; height: 44rpx; border-radius: 12rpx; border: 1px solid rgba(255, 255, 255, 0.38); }
.todo-item--completed .todo-check { background: rgba(0, 200, 120, 0.25); border-color: rgba(0, 200, 120, 0.7); }
.todo-text { flex: 1; font-size: 26rpx; line-height: 1.3; }
.todo-delete-button { height: 56rpx; padding: 0 18rpx; border-radius: 14rpx; background: rgba(255, 100, 100, 0.15); border: 1px solid rgba(255, 100, 100, 0.35); }
.todo-footer { display: flex; justify-content: flex-start; margin-top: 4rpx; }
.todo-clear-completed { height: 64rpx; padding: 0 22rpx; border-radius: 18rpx; background: var(--s1); border: 1px solid var(--b2); }
.todo-add-button-text { font-size: 26rpx; }
.todo-delete-text { font-size: 22rpx; color: rgba(255, 200, 200, 0.95); }
.todo-clear-completed-text { font-size: 24rpx; color: rgba(255, 255, 255, 0.85); }
.todo-check-icon { font-size: 30rpx; color: rgba(255, 255, 255, 0.95); }Key Differences Compared to Vue
- Core Vue features (such as
ref,computed, etc.) can now be imported directly from vue-lynx. - Unlike the web, standard HTML elements are not used (like react-native): <div> → <view> <span> → <text>
- Event handling works differently: @click → :bindtap
Current Limitations
Being still in the pre-alpha phase, some Vue features are not yet supported, including:
KeepAliveTeleportTransitionv-bind
Final Comment
Vue-Lynx clearly represents a big step toward native mobile development for Vue developers. At the moment, the Lynx ecosystem is still limited and many features are under development, but the potential is huge. It will be interesting to follow the updates and see how this technology evolves over the next years.
Github :
https://github.com/micbianchi97/todo-vue-lynx
Thanks for reading! 😊






