Getting started: Nuxt
Flash-free SSR: the theme lives in a cookie, and useHead writes it on <html> in the server-rendered HTML; styles go through css in nuxt.config.ts
1. Create the app
Tested with Nuxt 4.6. The scaffolder installs dependencies for you.
npm create nuxt@latest -- my-app --template minimal --packageManager npm --no-gitInit --modules=
cd my-app
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
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
css: ['valyro-ui/styles.css'],
})
4. Theme in a cookie
A cookie rather than localStorage: the server reads the choice and sends <html> with the right attributes right away, so hydration matches. decode lets through only known values, so a tampered cookie falls back to the default. In Nuxt 4, composables live in app/composables and are auto-imported.
export type UiTheme = 'vercel' | 'glass'
export type UiMode = 'light' | 'dark'
const maxAge = 60 * 60 * 24 * 365
export function useUiTheme() {
// Cookies (not localStorage) so the server renders the saved choice into <html> on the first response.
// `decode` whitelists values, so a tampered cookie falls back to the default instead of an unknown theme.
const theme = useCookie<UiTheme>('ui-theme', {
default: () => 'glass',
decode: (value) => (value === 'vercel' ? 'vercel' : 'glass'),
maxAge,
sameSite: 'lax',
})
const mode = useCookie<UiMode>('ui-mode', {
default: () => 'light',
decode: (value) => (value === 'dark' ? 'dark' : 'light'),
maxAge,
sameSite: 'lax',
})
return {
theme,
mode,
toggleTheme: () => {
theme.value = theme.value === 'glass' ? 'vercel' : 'glass'
},
toggleMode: () => {
mode.value = mode.value === 'light' ? 'dark' : 'light'
},
}
}
5. Attributes on <html> and markup
Call useHead once, in the root component, so head entries aren't duplicated. On the client, the attributes update about a frame after the click. 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.
<script setup lang="ts">
const { theme, mode, toggleTheme, toggleMode } = useUiTheme()
useHead({
htmlAttrs: { lang: 'en', 'data-theme': theme, 'data-mode': mode },
})
</script>
<template>
<div>
<NuxtRouteAnnouncer />
<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>
</div>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: 16px;
max-width: 560px;
padding: 32px;
}
.toolbar {
display: flex;
gap: 8px;
}
</style>
6. Build and run
npm run build
node .output/server/index.mjs
7. Type checking (optional)
The minimal template has no TypeScript dependencies. vue-tsc doesn't support TypeScript 7 yet, so the version is pinned.
npm add -D typescript@~6.0.0 vue-tsc
npx nuxt typecheck