Server-rendered

Overview

Choose server-rendered forms when you want the simplest Vue integration and you are happy for Formie to keep owning the form UI.

If you want to see server-rendered forms in a fuller app setup, use the Vue starter as a working example.

In this setup:

  • Vue owns the host component
  • Formie still owns the rendered HTML inside it
  • the browser package still owns validation, submit flow, and browser-side behavior

Browser events and browser modules still apply for server-rendered forms. Use the Browser docs when you need deeper browser behavior extension points.

Component

This is the simplest HTML-mode entry point.

vue
<script setup lang="ts">
import { FormieForm } from '@verbb/formie-vue';
import '@verbb/formie-browser/css/formie.css';

function onSuccess(result: unknown) {
  console.log('Submit ok:', result);
}

function onError(result: unknown) {
  console.log('Submit failed:', result);
}
</script>

<template>
  <FormieForm
    transport="rest"
    endpoint="https://formie.test"
    form-handle="contactForm"
    theme="formie"
    :on-success="onSuccess"
    :on-error="onError"
  />
</template>

In templates, use kebab-case for multi-word props (form-handle, theme-config). The underlying options match the React adapter.

Props

<FormieForm /> supports these HTML-mode props:

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless :source="{ payload }"Chooses how the HTML payload is loaded.
endpointstringYes, unless :source="{ payload }"For REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless :source="{ payload }"Handle of the form to load.
refreshTokensbooleanNoRefreshes CSRF, request, render, and captcha tokens as Formie needs them.
localestringNoAdvanced override for locale-sensitive rendering and formatting. Most setups can rely on siteId.
siteIdnumberNoRequests the form for a specific site.
autoVisiblebooleanNoReveals the mounted root automatically when it becomes ready.
theme'formie' | 'none'NoUses the shipped browser theme or skips it.
themeConfigRecord<string, unknown>NoPasses additional theme configuration to the browser theme layer.
source{ payload: FormEndpointPayload }NoUses a preloaded HTML payload instead of loading one during mount.
classNamestringNoAdds a class to the host element that Vue renders.

With transport="graphql", GraphQL only loads the HTML payload. Submit still uses the rendered form action rather than a GraphQL mutation.

Callback props and events

For the common path, start with onReady, onSuccess, and onError.

<FormieForm /> supports the same callbacks as props (onReady, onSuccess, …) and emits matching events (ready, success, …). Use either style.

Name (prop)Emit nameDescription
onMountmountCalled after the HTML form instance mounts. Receives a FormieFormInstance.
onReadyreadyCalled when the mounted instance is ready for use. Receives a FormieFormInstance.
onUnmountunmountCalled after the form is unmounted.
onResultresultCalled for every submit result. Receives a FormSubmitResult.
onSuccesssuccessCalled when submit succeeds. Receives a FormSubmitResult.
onErrorerrorCalled when submit fails. Receives a FormSubmitResult.
onSubmitResultsubmit-resultCalled for every submit result. Receives a FormSubmitResult.
onSubmitSuccesssubmit-successCalled when submit succeeds. Receives a FormSubmitResult.
onSubmitErrorsubmit-errorCalled when submit fails. Receives a FormSubmitResult.
onEventeventCalled for browser events exposed through the Vue wrapper. Receives a FormieVueEvent.

The onSubmit* props and submit-* events remain available as the more explicit lower-level aliases.

vue
<script setup lang="ts">
import { FormieForm } from '@verbb/formie-vue';

function onReady(instance: unknown) {
  console.log('Mounted:', (instance as { id: string }).id);
}

function onSuccess(result: unknown) {
  console.log('Submit ok:', result);
}

function onEvent(event: { name: string; payload: unknown }) {
  console.log(event.name, event.payload);
}
</script>

<template>
  <FormieForm
    transport="rest"
    endpoint="https://formie.test"
    form-handle="contactForm"
    theme="formie"
    :on-ready="onReady"
    :on-success="onSuccess"
    :on-event="onEvent"
  />
</template>

Template-only equivalent with v-on:

vue
<template>
  <FormieForm
    transport="rest"
    endpoint="https://formie.test"
    form-handle="contactForm"
    theme="formie"
    @ready="(instance) => console.log('Mounted:', instance.id)"
    @success="(result) => console.log('Submit ok:', result)"
    @event="(event) => console.log(event.name, event.payload)"
  />
</template>

Advanced composable

useFormieHtml() mounts the same server-rendered browser behavior, but lets your component own the host ref and imperative API directly.

vue
<script setup lang="ts">
import { useFormieHtml } from '@verbb/formie-vue';
import '@verbb/formie-browser/css/formie.css';

const { rootRef } = useFormieHtml({
  transport: 'rest',
  endpoint: 'https://formie.test',
  formHandle: 'contactForm',
  theme: 'formie',
});
</script>

<template>
  <div ref="rootRef" />
</template>

This is still the server-rendered path, but it is the lower-level escape hatch rather than the recommended starting point.

Options

useFormieHtml() accepts the same HTML mount options as the browser client, except mode is fixed to 'server-rendered':

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless payloadChooses how the HTML payload is loaded.
endpointstringYes, unless payloadFor REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless payloadHandle of the form to load.
payloadFormEndpointPayloadNoUses a preloaded HTML payload instead of loading one during mount.
refreshTokensbooleanNoRefreshes CSRF, request, render, and captcha tokens as Formie needs them.
localestringNoAdvanced override for locale-sensitive rendering and formatting. Most setups can rely on siteId.
siteIdnumberNoRequests the form for a specific site.
autoVisiblebooleanNoReveals the mounted root automatically when it becomes ready.
theme'formie' | 'none'NoUses the shipped browser theme or skips it.
themeConfigRecord<string, unknown>NoPasses additional theme configuration to the browser theme layer.

Return value

The composable returns:

NameTypeDescription
rootRefRef<HTMLElement | null>Host element ref that Formie mounts into.
state.instanceShallowRef<FormieFormInstance | null>Mounted instance, or null before mount.
state.isMountedRef<boolean>Whether the form is currently mounted.
state.errorRef<Error | null>Mount error, if mount failed.
submitfunctionImperative submit access for the mounted form. Accepts an optional FormAction and resolves to FormSubmitResult | null.

Events with the composable

The composable does not expose callback props. Subscribe to browser events from state.instance instead:

vue
<script setup lang="ts">
import { watch } from 'vue';
import { useFormieHtml } from '@verbb/formie-vue';

const form = useFormieHtml({
  transport: 'rest',
  endpoint: 'https://formie.test',
  formHandle: 'contactForm',
  theme: 'formie',
});
const { rootRef } = form;

watch(
  () => form.state.instance.value,
  (instance, _previous, onCleanup) => {
    if (!instance) {
      return;
    }

    const unsubscribe = instance.on('formie:submit:result', (result) => {
      console.log('Submit result:', result);
    });

    onCleanup(unsubscribe);
  },
  { immediate: true },
);
</script>

<template>
  <div ref="rootRef" />
</template>

Advanced client

Use createVueFormieClient() when you need lower-level browser-client control instead of the Vue composable wrapper:

vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { createVueFormieClient } from '@verbb/formie-vue';
import '@verbb/formie-browser/css/formie.css';

const client = createVueFormieClient();
const root = ref<HTMLElement | null>(null);

onMounted(async () => {
  if (!root.value) {
    return;
  }

  await client.mount(root.value, {
    mode: 'server-rendered',
    transport: 'rest',
    endpoint: 'https://formie.test',
    formHandle: 'contactForm',
    theme: 'formie',
  });
});

onBeforeUnmount(async () => {
  if (!root.value) {
    return;
  }

  await client.unmount(root.value);
});
</script>

<template>
  <div ref="root" />
</template>

This returns the same browser client surface as @verbb/formie-browser, including mount(), unmount(), update(), scan(), observe(), and module registration.

Preloaded payloads

If your app already fetched the HTML payload, pass it into Vue instead of fetching it again during mount:

vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { FormieForm, type FormEndpointPayload } from '@verbb/formie-vue';
import '@verbb/formie-browser/css/formie.css';

async function loadFormPayload(endpoint: string, handle: string): Promise<FormEndpointPayload> {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      handle,
    }),
  });

  if (!response.ok) {
    throw new Error('Failed to load form payload.');
  }

  return response.json();
}

const payload = ref<FormEndpointPayload | null>(null);

onMounted(() => {
  void loadFormPayload('https://formie.test/your-render-endpoint', 'contactForm').then((result) => {
    payload.value = result;
  });
});
</script>

<template>
  <FormieForm v-if="payload" :source="{ payload }" theme="formie" />
</template>

For composable usage, pass the same payload as payload instead of :source="{ payload }".

Last updated: May 6, 2026, 3:46 PM