Skip to main content
PolyUI

React · Vue · Svelte · Solid

Same Component.
Four Frameworks.

一个令牌文件,覆盖所有内容。运行时生效,无需重建。Props 在 React、Vue、Svelte 和 Solid 之间完全对齐。

React / Vue / Svelte / SolidTailwind v4 + OKLCH tokensMIT License
LoginCard.tsx
// @polyui/reactimport { Card, Input, Button, Label } from "@polyui/react"
export function LoginCard() {
  return (
    <Card>
      <Label>Email</Label>
      <Input type="email" />
      <Label>Password</Label>
      <Input type="password" />
      <Button variant="default">Sign in</Button>
    </Card>
  )
}
LIVE
:root {
  --primary: oklch(0.72 0.18 55);
  --radius:  8px;
}

为什么会有 PolyUI

你见过这个吗?

同一套设计,四个仓库,四套令牌,各自升级,互不兼容。改一个按钮颜色,就是四个 PR。

现状 · 四套各自为政

shadcn/ui

React

npx shadcn@latest add button

shadcn-vue

Vue

npx shadcn-vue@latest add button

shadcn-svelte

Svelte

npx shadcn-svelte@latest add button

shadcn-solid

Solid

npx shadcn-solid@latest add button

令牌写法各异。升级节奏不同。改一处,四处返工。

PolyUI · 单一真相来源

React

Base UI

Vue

Reka UI

Svelte

Bits UI

Solid

Kobalte

@polyui/tokens + @polyui/core

--primary · --radius · variants · props 契约

一次改令牌。四个框架同步更新。

架构

三层分离。

理解这三层,就理解了 PolyUI。第 1、2 层完全共享;第 3 层适配框架,但足够薄。

Layer 1 · 纯 CSS

@polyui/tokens · 令牌层

纯 CSS 变量。你的 token.css 覆盖这一层。CSS 层叠胜出,无需重建。

:root {
  --primary: oklch(0.72 0.18 55);
  --radius:  8px;
}
Layer 2 · 框架无关

@polyui/core · 配方层

纯 JS 函数。四个框架包都 import 它。"哪个 variant 对应哪些 class"只写一次。

export const buttonVariants = tv({
  base: "inline-flex items-center…",
  variants: { variant: { default: "bg-primary…" } },
})
Layer 3

框架适配层

各框架用最成熟的 headless 原语实现交互与可访问性。只负责"把令牌和 variants 渲染成 DOM"。

import { Button } from "@polyui/react"
// @polyui/vue · @polyui/svelte · @polyui/solid

第 1、2 层 100% 共享。第 3 层适配,但足够薄。这就是全部秘密。

Tier 1 · 运行时令牌

一个文件。全部改变。

修改左侧 token.css,右侧组件即时响应。不需要构建。这就是你在项目里做的事情。

token.css

试着改改 --primary 的色相数字。

实时预览
LIVE
默认成功信息
卡片标题

所有圆角、颜色都实时响应 token.css 变化。

Props 契约

语法不同。Props 一致。

用比登录卡更复杂的 Dialog 演示:哪怕是有状态的交互组件,PolyUI 的 props 命名在四个框架中始终对齐。

React
// @polyui/react
import { Dialog, DialogContent, DialogTitle, Button } from "@polyui/react"
function ConfirmDelete({ open, onClose }) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogTitle>Delete item</DialogTitle>
<p>This action cannot be undone.</p>
// ← variant, DialogTitle, open: same in all 4
<Button variant="destructive" onClick={onClose}>
Confirm
</Button>
</DialogContent>
</Dialog>
)
}
Vue
<!-- @polyui/vue -->
<script setup>
import { Dialog, DialogContent, DialogTitle, Button } from "@polyui/vue"
const props = defineProps(['open'])
const emit = defineEmits(['update:open'])
</script>
<template>
<Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent>
<DialogTitle>Delete item</DialogTitle>
<p>This action cannot be undone.</p>
<!-- ← variant, DialogTitle, open: same in all 4 -->
<Button variant="destructive" @click="emit('update:open', false)">
Confirm
</Button>
</DialogContent>
</Dialog>
</template>
Svelte
<!-- @polyui/svelte -->
<script>
import { Dialog, DialogContent, DialogTitle, Button } from "@polyui/svelte"
export let open = false
</script>
<Dialog bind:open>
<DialogContent>
<DialogTitle>Delete item</DialogTitle>
<p>This action cannot be undone.</p>
<!-- ← variant, DialogTitle, open: same in all 4 -->
<Button variant="destructive" onclick={() => open = false}>
Confirm
</Button>
</DialogContent>
</Dialog>
Solid
// @polyui/solid
import { Dialog, DialogContent, DialogTitle, Button } from "@polyui/solid"
function ConfirmDelete(props) {
return (
<Dialog open={props.open()} onOpenChange={props.onClose}>
<DialogContent>
<DialogTitle>Delete item</DialogTitle>
<p>This action cannot be undone.</p>
// ← variant, DialogTitle, open: same in all 4
<Button variant="destructive" onClick={props.onClose}>
Confirm
</Button>
</DialogContent>
</Dialog>
)
}

以上四段代码的渲染结果:

删除项目

此操作无法撤销。

语法各异。组件相同。

诚实边界

我们不假装做到的事

因为我们都见过:在脚注里发现限制,比从一开始就知道,代价要大得多。

我们不是 Mitosis。

PolyUI 不是"一份源码编译到四个框架"的方案。逻辑按框架分别实现(Base UI / Reka / Bits / Kobalte)。我们只是把这层压到最薄。可控性优先于魔法。

维护成本是 N 倍。

非引擎驱动的组件,最坏要写四遍交互逻辑。所以高级组件强烈优先选 TanStack / Embla / Tiptap 这类自带多框架适配器的引擎。

Charts 是视觉对齐,不是引擎统一。

图表生态没有一个可以覆盖四个框架的统一引擎。令牌和视觉外观保持一致,但渲染引擎按框架最优选。这是已知权衡,不是 bug。

Svelte / Solid 比 React 晚。

React headless 原语生态最成熟。部分组件会先发布 React / Vue,Svelte / Solid 版本稍后跟进。Roadmap 里的 M4 里程碑会补齐。

我们写这个区块,是因为我们自己也看过太多次"脚注里的真相"。

快速开始

加入你的项目

选择你的框架。粘贴命令。开始写组件。

$pnpm dlx polyui init --framework next
$pnpm dlx polyui add button card dialog