Skip to content
Forms, switch product

Nuxt

GolemUI renders on the server in Nuxt with the same packages and the same <GuiForm> component as a client-only Vue app. Verified with Nuxt 4 and Vue 3.5. The contract the adapter follows is on the Vue SSR page; this page is the wiring. Requires GolemUI 1.5.0 or newer.

Terminal window
npx degit golemui/golemui/templates/nuxt my-app
cd my-app
npm install
npm start

Open http://localhost:3000 and view the page source: the form markup arrives without JavaScript. The same template runs in StackBlitz. Node.js 22.12 or newer.

Terminal window
npm i @golemui/core @golemui/gui-shared @golemui/vue @golemui/gui-vue @golemui/gui-components --save

The widgets have to be in the registry before the first render, on the server and on the client. A server render cannot wait for a widget’s dynamic import while it produces the page, and the client would otherwise hydrate an empty tree over markup that is not empty. Nuxt awaits an async plugin before the first render on both sides.

app/plugins/golemui.ts
import { preloadFormWidgets } from '@golemui/core';
import { widgetLoaders } from '@golemui/gui-vue';
export default defineNuxtPlugin(async () => {
await preloadFormWidgets({ widgetLoaders });
});

Load the stylesheet through css. GolemUI never injects it. data-theme="auto" follows the OS color scheme.

export default defineNuxtConfig({
app: {
head: {
htmlAttrs: { lang: 'en', 'data-theme': 'auto' },
},
},
css: ['@golemui/gui-components/index.css'],
});
app/pages/index.vue
<script setup lang="ts">
import type { FormEvent } from '@golemui/core';
import { GuiForm } from '@golemui/gui-vue';
import { gui, type GuiFormInitConfig } from '@golemui/gui-shared';
const formDef = [
gui.inputs.dropdown('country', {
label: 'Country',
items: ['United States', 'Japan', 'Brazil', 'France'],
}),
gui.inputs.checkbox('pets', {
label: 'Travelling with pets?',
include: { when: '!!$form.country' },
}),
gui.actions.button({ label: 'Submit', uid: 'submit', onClick: () => 'handleSubmit' }),
];
const config: GuiFormInitConfig = {
formName: 'signup', // keeps the form id predictable on both sides
formDef,
};
function onFormEvent(event: FormEvent) {
if (event.name === 'handleSubmit') console.log('Submitted:', event.data);
}
</script>
<template>
<GuiForm :config="config" @form-event="onFormEvent" />
</template>
app/pages/index.vue
<script setup lang="ts">
import type { FormEvent } from '@golemui/core';
import { GuiForm } from '@golemui/gui-vue';
import formDef from './signup.json';
const config = {
formName: 'signup', // keeps the form id predictable on both sides
formDef,
};
function onFormEvent(event: FormEvent) {
if (event.name === 'handleSubmit') console.log('Submitted:', event.data);
}
</script>
<template>
<GuiForm :config="config" @form-event="onFormEvent" />
</template>

<GuiForm> needs no compiler configuration. Only a component of your own that places a raw gui-* tag in its template needs the Vue compiler to leave those tags alone:

nuxt.config.ts
export default defineNuxtConfig({
vue: {
compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') },
},
});

Object props on such a tag must be bound with .prop. Bound as an attribute, the server stringifies them into the markup:

<gui-select :options.prop="languages" @change="onLanguageChanged" />

The gui-* elements can also render their content on the server, so labels, inputs and values are in the HTML. It is optional. The fields are complete in the HTML before any JavaScript runs, and the page doesn’t change when the elements upgrade; in exchange, the HTML is bigger and the server does more work on each request. Without it, the page stays as described on the Vue SSR page.

To turn it on, install @lit-labs/ssr and add the GolemUI Components module:

nuxt.config.ts
export default defineNuxtConfig({
modules: ['@golemui/gui-components/nuxt'],
css: ['@golemui/gui-components/index.css'],
});

The module also sets the compiler options for the gui-* tags. To use it for those alone, set golemui: { serverRendering: false }. A value the adapter binds as a property, such as the options of a select, is not in the HTML: the select shows its placeholder until it upgrades. What the elements render on the server, and its limits, are in Server rendering in the GolemUI Components docs.

  • formName is optional on Vue 3.5 and newer, where the adapter derives a stable id on both sides. Set it when the id in the markup must be predictable.
  • Widget handlers, formEvent and formSubmit never run on the server. onLoad fires once the widget has mounted in the browser.
  • formHealth is called once during the server render with the initial health, because the component subscribes in setup(). Keep the handler free of DOM access.
  • i18next is a module-level singleton. On the server, every request re-initializes a shared instance. Scope the translator per request when the app serves several languages. See i18n.

Keep the loaders in one module-scope object, spread it into the plugin’s preload call and pass the same object as customWidgetLoaders in every form config. See Custom widget loaders.

app/plugins/golemui.ts
import { customWidgetLoaders } from '~/utils/custom-widget-loaders';
export default defineNuxtPlugin(async () => {
await preloadFormWidgets({
widgetLoaders: { ...widgetLoaders, ...customWidgetLoaders },
});
});