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