Installation
Setup
Add to a Vue-Lynx project
Install the @vyui/core package
pnpm add @vyui/core
yarn add @vyui/core
bun add @vyui/core
deno add npm:@vyui/core
npm install @vyui/core
Follow VyUI worklets in the Lynx build
VyUI components register 'main thread' worklets (gestures, drag, animation). vue-lynx's main-thread loader skips node_modules unless a package is allowlisted — without this, the first gesture throws TypeError: cannot read property 'bind' of undefined. Requires vue-lynx@^0.4.2.
pluginVueLynx({
includeWorkletPackages: ['@vyui/core', '@vyui/kit'],
})
Use a primitive
Primitives are unstyled. Compose them with your own classes, tokens, or design system.
<script setup>
import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from '@vyui/core'
import { ref } from 'vue'
const value = ref(50)
</script>
<template>
<SliderRoot v-model="value" :max="100">
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</SliderRoot>
</template>
Add styled components with @vyui/kit
Install both packages
pnpm add @vyui/core @vyui/kit
yarn add @vyui/core @vyui/kit
bun add @vyui/core @vyui/kit
deno add npm:@vyui/core npm:@vyui/kit
npm install @vyui/core @vyui/kit
@vyui/kit depends on @vyui/core, but it does not re-export the full core surface. Import styled Vy* components from kit, and import raw primitives or utilities directly from core unless the kit docs call out a specific Vy* convenience alias.
Configure Tailwind
@vyui/kit ships its styling as a Tailwind preset. Add it alongside the Lynx preset, and feed the kit ui-* state markers into the Lynx preset's uiVariants plugin — the class-based replacements for Lynx-incompatible data-[state] selectors. Without this step components render unstyled.
import type { Config } from 'tailwindcss'
import { createLynxPreset } from '@lynx-js/tailwind-preset'
import { createVyuiPreset, VYUI_UI_STATES } from '@vyui/kit/tailwind'
const lynxPreset = createLynxPreset({
lynxUIPlugins: {
uiVariants: {
prefixes: defaults => ({ ...defaults, ui: [...defaults.ui, ...VYUI_UI_STATES] }),
},
},
})
export default {
// Scan the kit + core component sources so their utility classes aren't purged.
content: [
'./src/**/*.{vue,js,ts}',
'./node_modules/@vyui/kit/dist/**/*.js',
'./node_modules/@vyui/core/dist/**/*.js',
],
presets: [lynxPreset, createVyuiPreset()],
} satisfies Config
Import the theme stylesheet
@vyui/kit/style.css defines the default design tokens using Tailwind theme() calls, so pull it in through your Tailwind pipeline with a CSS @import — not a plain JS import. Put any --ui-color-* overrides after it.
@tailwind base;
@tailwind utilities;
/* @vyui/kit default theme tokens — overrides go below this line. */
@import '@vyui/kit/style.css';
Register Vy UI
Vy UI needs its theme config provided once at your app entry. On Vue-Lynx — the primary target — do this with provideVyUI and import each component from its own deep entry where you use it (@vyui/kit/button), so the bundler ships only what you reference — see Bundle size & deep imports. Kit's defaults use lucide icons, so register that set up front, and install the Intl polyfill for Lynx's PrimJS engine.
import { createApp } from 'vue-lynx'
import { installIntlPolyfill, registerIconSet } from '@vyui/core'
import { provideVyUI } from '@vyui/kit'
import lucide from '@iconify-json/lucide/icons.json'
import App from './App.vue'
import './index.css'
installIntlPolyfill()
registerIconSet('lucide', lucide)
const app = createApp(App)
provideVyUI(app)
app.mount()
Then import each component from its own entry where you use it:
<script setup>
import { VyButton } from '@vyui/kit/button'
</script>
<template>
<VyButton>Tap me</VyButton>
</template>
The barrel (import { VyButton } from '@vyui/kit') works identically, but on Vue-Lynx it ships every component — see Bundle size & deep imports below.
Bundle size & deep imports
Every component is published twice: through the @vyui/kit barrel and through its own deep entry (@vyui/kit/button, @vyui/kit/tray, …). The bindings are identical — the canonical Vy* name — so choosing one is a specifier swap.
On the web, either is fine; a normal tree-shaking bundler drops what you don't reference from the barrel. On Vue-Lynx it does not. The main-thread worklet pipeline prunes by sideEffects globs over whatever is reached, and a bare side-effect import erases export-level usage — so a single barrel import pulls the whole set into both the background bundle and the worklet slice. Deep entries are the only way to ship less:
| Import | Modules reached | Worklet registrations |
|---|---|---|
@vyui/kit/button | ~37 | ~26 |
@vyui/kit (barrel) | ~294 | ~118 |
So on Vue-Lynx, prefer deep imports for components. One barrel import anywhere in a module re-pulls everything, so keep component imports deep throughout. (The build-only entries @vyui/kit/config and @vyui/kit/tailwind are already isolated and never pull component code.)
app.use(VyUI), which registers every Vy* component globally so <VyButton> works without an import — convenient, but it pulls the entire component set into your bundle. Vue-Lynx's createApp has no app.component, so app.use(VyUI) degrades to theme-only there (with a dev warning) — provideVyUI + named imports is the native path. Register just a subset globally with app.use(VyUI, { components: { VyButton } }).Runtime modes
@vyui/kit runs on any Vue-compatible runtime, but only some expose app.component. Pick the entry that matches yours:
| Runtime | Entry | Components |
|---|---|---|
| Vue-Lynx / minimal (native path) | provideVyUI(app, config) | Deep imports (import { VyButton } from '@vyui/kit/button') |
| Full Vue (web) | app.use(VyUI, config) | Registered globally |
| Build (Tailwind) | createVyuiPreset(config) | — (generates the class surface) |
Vue-Lynx's createApp has no app.component, so app.use(VyUI) degrades to theme-only there (with a dev warning) — provideVyUI is the blessed native path. Runtime config selects from the classes Tailwind already emitted; it never creates styling.
Single-source config with defineVyuiConfig
The color set is meaningful to both planes: the Tailwind preset must generate the classes, and the runtime must select the same set. Author it once with defineVyuiConfig (from the light @vyui/kit/config entry — safe to import in a Tailwind config; it never pulls component code) and feed the result to both:
import { defineVyuiConfig } from '@vyui/kit/config'
export default defineVyuiConfig({
theme: {
primary: 'orange',
gray: 'stone',
colors: ['primary', 'secondary', 'success', 'info', 'warning', 'error'],
},
components: {
button: { slots: { base: 'rounded-xl' } },
},
})
import vyuiConfig from './vyui.config'
import { createVyuiPreset } from '@vyui/kit/tailwind'
export default {
presets: [lynxPreset, createVyuiPreset(vyuiConfig)],
}
import vyuiConfig from './vyui.config'
import { provideVyUI } from '@vyui/kit' // or: app.use(VyUI, vyuiConfig) on web
provideVyUI(app, vyuiConfig)
Mount the overlay & toast hosts
VyModal, VyDrawer, VyPopover and friends render into a portal host, and VyToast needs a toast provider — the plugin does not wire these up for you. Mount both once near the root:
<script setup>
import { OverlayRoot, ToastProvider } from '@vyui/core'
</script>
<template>
<ToastProvider>
<!-- your app -->
</ToastProvider>
<!-- Portal host for overlays; keep it a sibling at the app root. -->
<OverlayRoot />
</template>
Use a Vy* component
<script setup>
import { VyButton, VySlider } from '@vyui/kit'
import { ref } from 'vue'
const value = ref(50)
</script>
<template>
<VySlider v-model="value" :max="100" />
<VyButton variant="solid" color="primary">Save</VyButton>
</template>
Options
Kit ships sensible defaults — you only need to reach for these when you want to override theme tokens, swap a primary color, or change a component's default variant.
theme
Pass component overrides under ui.<component> to deep-merge them into the default Tailwind Variants theme. Every Vy* component reads from this — no rebuild required. Like Nuxt UI, everything lives under the single ui namespace.
createApp(App).use(VyUI, {
ui: {
button: {
defaultVariants: { color: 'primary' },
},
},
})
colors
Override the semantic color slots (primary, secondary, success, info, warning, error, neutral) by redefining the --ui-color-{semantic}-{shade} CSS variables in your own stylesheet, or add a brand-new color slot (e.g. tertiary) with defineVyuiConfig — see Theming → Colors for the full walkthrough, including the type-safety and codegen steps.
Troubleshooting
TypeError: cannot read property 'bind' of undefined
The app builds fine, then crashes on the main thread the first time a
worklet-driven component runs — dragging a VySortable, a gesture, an
animation:
main-thread.js exception: TypeError: cannot read property 'bind' of undefined
Cause. VyUI ships those handlers as 'main thread' worklets inside
@vyui/core / @vyui/kit. vue-lynx's main-thread loader only walks
first-party source, so worklets living in node_modules never get registered —
and the main thread then tries to .bind a worklet that isn't there.
Fix. Allowlist the packages in your Lynx build (requires vue-lynx@^0.4.2):
pluginVueLynx({
includeWorkletPackages: ['@vyui/core', '@vyui/kit'],
})
If you set the project up with the CLI, run
npx @vyui/cli check — it audits your config and reports this (and other common
wiring gaps) with the fix, and exits non-zero so you can gate CI on it. init
adds the allowlist for you on new projects.