omi
Omi is a lightweight web components framework from Tencent that combines Custom Elements, JSX/function components, and signal-driven reactivity. It supports both OOP class components and data-oriented reactive functions, and produces standards-based components usable across React, Vue, and plain HTML.
MITPermissive — free to use in commercial and proprietary software, with attribution.View license →
Production readiness
4/5- Actively maintainedCommits in the last 6 months
- No known vulnerabilitiesNot yet scanned
- Clear, usable licenseMIT (permissive)
- Proven adoptionWidely used
- Has documentationDocumentation indexed
npm install omiOur analysis
Omi is a tiny web components framework that pairs the native Custom Elements standard with JSX, signal-based reactivity, and a small ecosystem (router, forms, suspense, directives, Tailwind UI kits).
When to use omi
Use Omi when you want to build framework-agnostic, standards-based UI components that can be dropped into React, Vue, or vanilla pages, and you prefer a lightweight signal-driven model with JSX over a heavier framework. It's a good fit for design systems and reusable widget libraries that must outlive any single framework.
When not to
If you're committed to a single ecosystem (React/Vue/Angular) and want its mature tooling, routing, SSR and hiring pool, a native framework is safer. For server-side rendering, large-scale SSR/streaming apps, or projects needing a vast component/library ecosystem, Omi's smaller community is a limitation. Lit is a more widely adopted alternative if you specifically want web components.
Strengths
- Outputs native Web Components that interoperate with any framework or no framework
- Signal-based fine-grained reactivity with both OOP and data-oriented styles
- Small bundle size and fast runtime
- Backed by Tencent with a fairly complete ecosystem: CLI, router, forms, icons, starters, Tailwind kits
- Uses Constructable Stylesheets for efficient shared styling
Trade-offs
- Much smaller community and mindshare than React/Vue/Lit, so fewer resources and third-party libs
- JSX-with-web-components hybrid model has a learning curve and some quirks (manual update() calls, tree-shaking caveats)
- Documentation is partly bilingual and scattered across many sub-packages
- Limited SSR story compared to mainstream frameworks
- Shadow DOM / Custom Element constraints (styling, forms, accessibility) apply
Maturity
A long-lived, actively maintained project with ~13k stars and corporate (Tencent) backing, a monorepo of supporting packages, and multiple starter kits. Mature relative to its niche, but the web-components-framework niche itself remains less mainstream than React/Vue.
English | 简体中文
📶 Signal-driven reactive programming by reactive-signal
⚡ Tiny size, Fast performance
🌐 Everything you need: Web Components, JSX, Function Components, Router, Suspense, Directive, Tailwindcss...
💒 Harness Constructable Stylesheets to easily manage and share styles
import { render, signal, tag, Component, h } from 'omi'
const count = signal(0)
function add() {
count.value++
}
function sub() {
count.value--
}
@tag('counter-demo')
export class CounterDemo extends Component {
static css = 'span { color: red; }'
render() {
return (
<>
<button onClick={sub}>-</button>
<span>{count.value}</span>
<button onClick={add}>+</button>
</>
)
}
}
Use this component:
import { h } from 'omi'
import './counter-demo'
render(<counter-demo />, document.body)
// or
import { CounterDemo, Other } from './counter-demo'
// Prevent tree Shaking when importing other things
render(<CounterDemo />, document.body)
// or
document.body.appendChild(document.createElement('counter-demo'))
Install
npm i omi
To quickly create an Omi + Vite + TS/JS project:
$ npx omi-cli init my-app # or create js project by: npx omi-cli init-js my-app
$ cd my-app
$ npm start # develop
$ npm run build # release
To quickly create an Omi + Router + Signal + Suspense + Tailwindcss + Vite + TS project:
$ npx omi-cli init-spa my-app
$ cd my-app
$ npm start # develop
$ npm run build # release
Packages
Core packages
omi- Implementation of omi framework.omi-form- Powerful, simple and cross frameworks form solution.lucide-omi- Lucide icon collection for omi.omiu- Hope to create the best web components. For example, the powerful vchart and vtableomi-router- Create SPA of omi framework.omi-cli- To quickly create an Omi + Vite + TS/JS project.
Starter kits (not published to npm)
omi-elements- Tailwind Element Omi UI KIT.omi-starter-spa- A starter repo for building single page app using Omi + OmiRouter + Tailwindcss + TypeScript + Vite + Prettier.omi-starter-ts- A starter repo for building web app or reusable components using Omi in TypeScript base on Vite.omi-starter-tailwind- A starter repo for building web app or reusable components using Omi + Tailwindcss + TypeScript + Vite.omi-starter-js- A starter repo for building web app or reusable components using Omi in JavaScript base on Vite.omi-vue- Vue SFC + Vite + OMI + OMI-WeUI.
Components
omi-weui- WeUI Components of omi.omi-auto-animate- Omi version of @formkit/auto-animate.omi-suspense- Handling asynchronous dependencies.
Directives
omi-transition- Applying animations when an component is entering and leaving the DOM.omi-ripple- A lightweight component for adding ripple effects to user interface elements.
Examples (not published to npm)
snake-game-2tier- SNake Game withSignalclasssnake-game-3tier- SNake Game with reactivity functionsomi-tutorial- Source code of omi tutorial.
If you want to help the project grow, start by simply sharing it with your peers!
Thank you!
Usage
TodoApp with reactivity functions
Data oriented programming
In data-oriented programming, the focus is on the data itself and the operations on the data, rather than the objects or data structures that hold the data. This programming paradigm emphasizes the change and flow of data, and how to respond to these changes. The TodoApp with reactivity functions is a good example of this, using the concepts of reactive programming, where the UI automatically updates to reflect changes in the data (i.e., the to-do list).
import { render, signal, computed, tag, Component, h } from 'omi'
const todos = signal([
{ text: 'Learn OMI', completed: true },
{ text: 'Learn Web Components', completed: false },
{ text: 'Learn JSX', completed: false },
{ text: 'Learn Signal', completed: false }
])
const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})
const newItem = signal('')
function addTodo() {
// api a
todos.value.push({ text: newItem.value, completed: false })
todos.update() // Trigger UI auto update
// api b, same as api a
// todos.value = [...todos.value, { text: newItem.value, completed: false }]
newItem.value = '' // Changing the value type can automatically update the UI
}
function removeTodo(index: number) {
todos.value.splice(index, 1)
todos.update() // Trigger UI auto update
}
@tag('todo-list')
class TodoList extends Component {
onInput = (event: Event) => {
const target = event.target as HTMLInputElement
newItem.value = target.value
}
render() {
return (
<>
<input type="text" value={newItem.value} onInput={this.onInput} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.value.map((todo, index) => {
return (
<li>
<label>
<input
type="checkbox"
checked={todo.completed}
onInput={() => {
todo.completed = !todo.completed
todos.update() // Trigger UI auto update
}}
/>
{todo.completed ? <s>{todo.text}</s> : todo.text}
</label>
{' '}
<button onClick={() => removeTodo(index)}>❌</button>
</li>
)
})}
</ul>
<p>Completed count: {completedCount.value}</p>
</>
)
}
}
render(<todo-list />, document.body)
Auto Import h
vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
esbuild: {
jsxInject: "import { h } from 'omi'",
jsxFactory: "h",
jsxFragment: "h.f"
}
})
You can inject code during construction, so you don't have to manually export h.
Define Cross Framework Component
The case of using Omi component in Vue is as follows:

my-counter.tsx:
import { tag, Component, h, bind } from 'omi'
@tag('my-counter')
class MyCounter extends Component {
static props = {
count: {
type: Number,
default: 0,
changed(newValue, oldValue) {
this.state.count = newValue
this.update()
}
}
}
state = {
count: null
}
install() {
this.state.count = this.props.count
}
@bind
sub() {
this.state.count--
this.update()
this.fire('change', this.state.count)
}
@bind
add() {
this.state.count++
this.update()
this.fire('change', this.state.count)
}
render() {
return (
<>
<button onClick={this.sub}>-</button>
<span>{this.state.count}</span>
<button onClick={this.add}>+</button>
</>
)
}
}
Using in Vue3
<script setup>
import { ref } from 'vue'
// import omi component
import './my-counter'
defineProps({
msg: String,
})
const count = ref(0)
const change = (e) => {
count.value = e.detail
}
</script>
<template>
<h1>{{ msg }}</h1>
<my-counter @change="change" :count="count" />
<p>
【Omi】
</p>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
【Vue】
</p>
</div>
</template>
If you fire the count-change in an Omi component:
this.fire('count-change', this.state.count)
To use the component and listen for events in Vue:
<my-counter @count-change="change" :count="count" />
Using in React
import { useState, useRef, useEffect } from 'react'
import useEventListener from '@use-it/event-listener'
import './my-counter'
function App() {
const [count, setCount] = useState(100)
const myCounterRef = useRef(null)
useEffect(() => {
const counter = myCounterRef.current
if (counter) {
const handleChange = (evt) => {
setCount(evt.detail)
}
counter.addEventListener('change', handleChange)
return () => {
counter.removeEventListener('change', handleChange)
}
}
}, [])
return (
<>
<h1>Omi + React</h1>
<my-counter count={count} ref={myCounterRef}></my-counter>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</>
)
}
export default App
Contributors
License
MIT © Tencent