Client-rendered

Overview

Choose client-rendered forms when Vue should render the visible form UI and you want full control over components, layout, and composables.

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

In this setup:

  • Vue renders from Formie's client definition
  • Formie owns state, page progression, validation, and submission behind it
  • your app owns the visible UI through Vue components, field components, slots, and composables

Component

Start with <FormieClientForm />:

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

<template>
  <FormieClientForm
    
    transport="rest"
    endpoint="https://formie.test"
    form-handle="contactForm"
  />
</template>

This is the simplest client-rendered entry point.

Props

<FormieClientForm /> supports these client-rendered props:

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless sourceChooses how the client definition envelope is loaded.
endpointstringYes, unless sourceFor REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless sourceHandle of the form to load.
siteIdnumberNoRequests the form for a specific site.
sourceFormieDefinitionSourceNoUses a preloaded client definition envelope and transport metadata.
componentsFormieVueComponentsNoReplaces top-level Form, Page, Field, or error-summary components.
fieldComponentsmapNoReplaces specific field-type renderers. Keys are FrontendFieldType values and values are Vue components.
slotsmapNoIntercepts smaller layout regions inside the default component tree. Keys are slot names and values are Vue components.
classNamestringNoAdds a class to the rendered form root.

Callback props and events

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

<FormieClientForm /> supports the same callbacks as props and emits matching events. Use either style.

Name (prop)Emit nameDescription
onMountmountCalled after the form instance mounts. Receives a FrontendFormInstance.
onReadyreadyCalled when the form instance is ready for use. Receives a FrontendFormInstance.
onUnmountunmountCalled after the form is unmounted.
onResultresultCalled for every submit result. Receives a FrontendSubmitResult.
onSuccesssuccessCalled when submit succeeds. Receives a FrontendSubmitResult.
onErrorerrorCalled when submit fails. Receives a FrontendSubmitResult.
onSubmitResultsubmit-resultCalled for every submit result. Receives a FrontendSubmitResult.
onSubmitSuccesssubmit-successCalled when submit succeeds. Receives a FrontendSubmitResult.
onSubmitErrorsubmit-errorCalled when submit fails. Receives a FrontendSubmitResult.
onEventeventCalled for client 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 { FormieClientForm } from '@verbb/formie-vue';

function onReady(formie: unknown) {
  console.log('Form ready:', formie);
}

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

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

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

Transport

Once you choose client-rendered forms, you also need to choose how the client definition envelope is loaded and how submissions are sent.

Use REST when:

  • you want the simplest transport story
  • you want the closest fit to the client-rendered controllers
  • you are wiring the app against Formie's standard frontend actions

Use GraphQL when:

  • your app already standardizes on GraphQL
  • you want transport to stay inside an existing GraphQL client workflow
  • you want to load formieClientForm yourself or through your existing data layer

For client-rendered forms, GraphQL covers more than the initial load. Formie also uses GraphQL mutations for submit, session refresh, and page changes.

GraphQL query

For GraphQL client-rendered forms, load formieClientForm:

graphql
query ClientForm($handle: String!, $siteId: Int) {
  formieClientForm(handle: $handle, siteId: $siteId) {
    schemaVersion
    definition
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}

That query returns the FrontendFormEnvelope Vue needs: schemaVersion, definition, and session.

Manual GraphQL mutations

If you use <FormieClientForm transport="graphql" />, Formie handles submit, session refresh, and page changes for you.

If your app owns the GraphQL client directly, use these mutations:

  • submitFormieClientForm
  • refreshFormieClientSession
  • setFormieClientPage

For example, submit uses submitFormieClientForm:

graphql
mutation SubmitForm($input: FormieClientSubmitInput!) {
  submitFormieClientForm(input: $input) {
    success
    submissionUid
    currentPageId
    nextPageId
    previousPageId
    isFinalPage
    errors
    messages
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}

Preloaded definition

If your app already fetched the client definition envelope, pass it into source instead of loading it again inside the form component:

vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { FormieClientForm, type FrontendFormEnvelope } from '@verbb/formie-vue';

const query = `
query ClientForm($handle: String!, $siteId: Int) {
  formieClientForm(handle: $handle, siteId: $siteId) {
    schemaVersion
    definition
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}`;

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

  if (!response.ok) {
    throw new Error('Failed to load client definition envelope.');
  }

  const body = await response.json();

  return body.data.formieClientForm;
}

const envelope = ref<FrontendFormEnvelope | null>(null);

onMounted(() => {
  void loadClientEnvelope('https://formie.test/api', 'contactForm').then((result) => {
    envelope.value = result;
  });
});
</script>

<template>
  <FormieClientForm
    v-if="envelope"
    
    :source="{
      definition: envelope,
      transport: {
        type: 'graphql',
        endpoint: 'https://formie.test/api',
        formHandle: 'contactForm',
      },
    }"
  />
</template>
Last updated: May 6, 2026, 3:46 PM