ValyroUI Русская версия

Getting started: Vue

Vue 3 + Vite without React: valyro-ui/styles.css, theme attributes on <html> and static component markup in templates. Interactive components run as a React island

1. Create the app

Tested with create-vue 3.24, Vue 3.5 and Vite 8.

npm create vue@latest -- --ts my-app
cd my-app
npm install

2. Install the package

You only need the package's CSS: valyro-ui/styles.css (plus the theme file if your theme isn't one of the two built-in ones). Fonts are separate files: the vercel theme (the ThemeProvider default) uses Geist, so import valyro-ui/fonts/geist.css and valyro-ui/fonts/geist-mono.css next to the styles, or text falls back to the system font; glass uses system fonts and needs none. npm installs react and react-dom into node_modules as peer dependencies, but they stay out of your bundle unless you import them. Until the package is published, install the tarball: npm install ../valyro-ui-0.1.0.tgz.

npm install valyro-ui

3. Add the styles

The template CSS paints body and sets the width of #app, covering the theme background (the theme paints it on <html>). Delete src/assets, src/components and the import './assets/main.css' line.

import 'valyro-ui/styles.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

4. Flash-free theme

data-theme (vercel, glass or a theme loaded from its own file) and data-mode (light or dark) on <html> turn the theme on: the --ui-* tokens and the page background depend on them. An inline script in <head> applies the saved choice before the first paint, even before the bundle loads.

<!DOCTYPE html>
<html lang="en" data-theme="glass" data-mode="light">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
    <script>
      // Runs before first paint: apply the saved choice so the page never flashes the default theme.
      (() => {
        const root = document.documentElement;
        const theme = localStorage.getItem('ui-theme');
        const mode = localStorage.getItem('ui-mode');
        if (theme === 'glass' || theme === 'vercel') root.dataset.theme = theme;
        if (mode === 'light' || mode === 'dark') root.dataset.mode = mode;
      })();
    </script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

5. Theme switcher

The composable reads the current <html> attributes (already set by the script) and, when they change, writes them back along with localStorage.

import { ref, watch } from 'vue'

export type ThemeName = 'vercel' | 'glass'
export type ThemeMode = 'light' | 'dark'

const root = document.documentElement

// The inline script in index.html has already applied the saved choice, so <html> is the source of truth.
const theme = ref<ThemeName>(root.dataset.theme === 'vercel' ? 'vercel' : 'glass')
const mode = ref<ThemeMode>(root.dataset.mode === 'dark' ? 'dark' : 'light')

watch(theme, (value) => {
  root.dataset.theme = value
  localStorage.setItem('ui-theme', value)
})

watch(mode, (value) => {
  root.dataset.mode = value
  localStorage.setItem('ui-mode', value)
})

export function useTheme() {
  return {
    theme,
    mode,
    toggleTheme: () => {
      theme.value = theme.value === 'glass' ? 'vercel' : 'glass'
    },
    toggleMode: () => {
      mode.value = mode.value === 'light' ? 'dark' : 'light'
    },
  }
}

6. Component markup

The markup is exactly what the React components render (Button, Badge, TextInput, Alert, Card). Only visual components work without React (see the list in the HTML + CSS guide); for any of them, the “Usage” example on the component's page shows the markup when this framework is selected. In Vue, paste it into the template as is.

<script setup lang="ts">
import { useTheme } from './useTheme'

const { theme, mode, toggleTheme, toggleMode } = useTheme()
</script>

<template>
  <main class="demo">
    <div class="toolbar">
      <button class="ui-button" data-variant="secondary" data-size="md" type="button" @click="toggleTheme">
        <span class="ui-button__label">Theme: {{ theme }}</span>
      </button>
      <button class="ui-button" data-variant="secondary" data-size="md" type="button" @click="toggleMode">
        <span class="ui-button__label">Mode: {{ mode }}</span>
      </button>
    </div>

    <button class="ui-button" data-variant="primary" data-size="md" type="button"><span class="ui-button__label">Deploy</span></button>
    <span class="ui-badge" data-color="green" data-variant="subtle" data-size="md"><span class="ui-badge__label">Ready</span></span>
    <div class="ui-field ui-text-input" data-size="md"><input aria-label="Email" placeholder="[email protected]" type="text" class="ui-field__input ui-text-input__input" /></div>
    <div role="status" class="ui-alert" data-variant="info">
      <span class="ui-alert__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="ui-icon"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg></span>
      <div class="ui-alert__content"><div class="ui-alert__title">Heads up</div><div class="ui-alert__description">Static markup, no React at runtime.</div></div>
    </div>
    <div class="ui-card ui-panel" data-variant="default" data-padding="md">
      <div class="ui-card__header"><h3 class="ui-card__title">Project</h3><p class="ui-card__description">Production deployment</p></div>
      <div class="ui-card__content">Body</div>
      <div class="ui-card__footer"><button class="ui-button" data-variant="secondary" data-size="md" type="button"><span class="ui-button__label">Open</span></button></div>
    </div>
  </main>
</template>

<style scoped>
.demo {
  display: grid;
  justify-items: start;
  gap: 16px;
  max-width: 560px;
  padding: 32px;
}

.toolbar {
  display: flex;
  gap: 8px;
}
</style>

7. Interactive components as a React island

Select, Switch, Dialog and other stateful components work only in React. You can mount them into a Vue component with createRoot — no JSX and no React plugin for Vite. The bundle grows by React 19 (≈ +82 kB gzip). Pass the current theme and mode to ThemeProvider: it writes them to <html>, and without them the island would switch the page to vercel/light.

npm install react react-dom
npm install -D @types/react @types/react-dom

8. Island wrapper

<script setup lang="ts" generic="P extends object">
import { createElement, type ComponentType } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { ThemeProvider } from 'valyro-ui'
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import { useTheme } from './useTheme'

const props = defineProps<{ component: ComponentType<P>; props: P }>()

const { theme, mode } = useTheme()
const container = useTemplateRef<HTMLDivElement>('container')
let root: Root | null = null

function render() {
  root?.render(
    createElement(ThemeProvider, {
      theme: theme.value,
      mode: mode.value,
      children: createElement(props.component, props.props),
    }),
  )
}

onMounted(() => {
  root = createRoot(container.value!)
  render()
})

// Re-render when the theme/mode changes or the parent passes new props.
watch([theme, mode, () => props.component, () => props.props], render)

onBeforeUnmount(() => {
  root?.unmount()
  root = null
})
</script>

<template>
  <div ref="container" data-react-island></div>
</template>

9. Island in a template

State lives in Vue: values go down as props, and changes come back through callbacks. vue-tsc type-checks the Select and Switch props.

<script setup lang="ts">
import { ref } from 'vue'
import { Select, Switch } from 'valyro-ui'
import ReactIsland from './ReactIsland.vue'

const region = ref('eu')
const notify = ref(true)
const regions = [
  { value: 'eu', label: 'Europe' },
  { value: 'us', label: 'United States' },
]
</script>

<template>
  <ReactIsland
    :component="Select"
    :props="{ 'aria-label': 'Region', options: regions, value: region, onValueChange: (v: string) => (region = v) }"
  />
  <ReactIsland
    :component="Switch"
    :props="{ label: 'Email notifications', checked: notify, onCheckedChange: (v: boolean) => (notify = v) }"
  />
  <p>Vue state: region = <b>{{ region }}</b>, notifications = <b>{{ notify }}</b></p>
</template>

10. Quiet build (optional)

With an island, the build prints a Rolldown warning for every package module marked "use client" (over a hundred). The directive is for React Server Components and is harmless in a client bundle; this filter hides only these warnings.

export default defineConfig({
  // …plugins and resolve as in the template…
  build: {
    rolldownOptions: {
      // valyro-ui marks its modules 'use client' for React Server Components; a client bundle can ignore it.
      onwarn(warning, warn) {
        if (warning.code !== 'MODULE_LEVEL_DIRECTIVE') warn(warning)
      },
    },
  },
})

11. Build

npm run build runs vue-tsc and vite build. Rolldown's warning about the "use client" directive when importing the package's JS is harmless (see the filter above).

npm run build
npx vite preview

Other frameworks