Getting Started

Installation

Install @vyui/core and @vyui/kit into a Vue-Lynx app.

Setup

Add to a Vue-Lynx project

Install the @vyui/core package

pnpm add @vyui/core

Follow VyUI worklets in the Lynx build

VyUI components register 'main thread' worklets for gestures, drag, and animation. vue-lynx's main-thread loader skips node_modules unless a package is allowlisted, and without that the first gesture throws TypeError: cannot read property 'bind' of undefined. Requires vue-lynx@^0.4.2.

lynx.config.ts
pluginVueLynx({
  includeWorkletPackages: ['@vyui/core', '@vyui/kit'],
})

Use a primitive

Primitives are unstyled. Compose them with your own classes, tokens, or design system.

App.vue
<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

@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, which supplies the class-based replacements for Lynx-incompatible data-[state] selectors. Without this step components render unstyled.

tailwind.config.ts
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 rather than a plain JS import. Put any --ui-color-* overrides after it.

src/index.css
@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.

src/index.ts
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:

SomeScreen.vue
<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, …). Both bindings expose the same 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:

ImportModules reachedWorklet 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 entry @vyui/kit/tailwind is already isolated and never pulls component code.)

On full Vue (web) you can instead call app.use(VyUI), which registers every Vy* component globally so <VyButton> works without an import. That's 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), which leaves provideVyUI plus named imports as 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:

RuntimeEntryComponents
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), making provideVyUI the native path. Runtime config selects from the classes Tailwind already emitted; it never creates styling.

Single-source config

The color set is meaningful to both planes: the Tailwind preset must generate the classes, and the runtime must select the same set. Author one plain config object and feed it to both. Palette selection and per-component tailwind-variants overrides share the same ui bag:

vyui.config.ts
import type { VyUIPluginOptions } from '@vyui/kit'

export default {
  ui: {
    primary: 'orange',
    gray: 'stone',
    colors: ['primary', 'secondary', 'success', 'info', 'warning', 'error'],
    button: { slots: { base: 'rounded-xl' } },
  },
} satisfies VyUIPluginOptions

The import type is erased at compile time, so the Tailwind config can import this file without pulling component code into the build path.

tailwind.config.ts
import vyuiConfig from './vyui.config'
import { createVyuiPreset } from '@vyui/kit/tailwind'

export default {
  presets: [lynxPreset, createVyuiPreset(vyuiConfig)],
}
src/index.ts
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, so mount both once near the root:

App.vue
<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

App.vue
<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, so you only need 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, and no rebuild is required. Like Nuxt UI, everything lives under the single ui namespace.

src/index.ts
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 such as tertiary in your vyui.config.ts. See Theming → Colors for the full walkthrough, including the type-safety and codegen steps.

Prefer copying component source into your app? Use the Vy UI CLI to initialize a registry-backed setup and add individual components.

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, whether that's dragging a VySortable, a gesture, or 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):

lynx.config.ts
pluginVueLynx({
  includeWorkletPackages: ['@vyui/core', '@vyui/kit'],
})

If you set the project up with the CLI, run npx @vyui/cli check. It audits your config, reports this and other common wiring gaps alongside the fix, and exits non-zero so you can gate CI on it. init adds the allowlist for you on new projects.