Modules

Build a Custom Module ​

Register a module in your application's trusted JavaScript bundle when a field needs additional browser behaviour. This example keeps a range input's output label in sync and removes its listener when the field disappears.

The field template must include data-formie-field-uid with the form-field instance UID, a range input and an output element. Formie's standard field wrapper supplies the UID attribute.

ts
import { createFormieClient, type BrowserModuleDefinition } from '@verbb/formie-browser';

const rating: BrowserModuleDefinition = {
    moduleId: 'acme:rating',
    version: 2,
    surfaces: ['server-rendered'],
    kind: 'field',
    match: ({ target }) => Boolean(target.querySelector('input[type="range"]')),
    async setup({ target }) {
        const input = target.querySelector<HTMLInputElement>('input[type="range"]')!;
        const output = target.querySelector('output')!;
        const sync = () => { output.textContent = input.value; };
        input.addEventListener('input', sync);
        sync();
        return { destroy: () => input.removeEventListener('input', sync) };
    },
};

const client = createFormieClient();
client.registerModule(rating);
await client.scan(document);

Declare its configuration in your PHP field's defineBrowserModules() method:

php
use verbb\formie\models\BrowserModule;

protected function defineBrowserModules(): array
{
    return [new BrowserModule([
        'key' => $this->uid . ':rating-primary',
        'moduleId' => 'acme:rating',
        'surfaces' => [BrowserModule::SURFACE_SERVER_RENDERED],
        'config' => ['max' => 10],
        'required' => true,
    ])];
}

Formie supplies the field target and includes the entry in the surface-specific manifest. Keep credentials and server-only settings out of config. The manifest cannot load a src URL; module code must already be registered or be resolved by Formie's trusted built-in import map.

For behaviour belonging to the whole form rather than a field, contribute a BrowserModule through the PHP manifest builder's registerModules event. The event exposes the form and requested surface. Contributions default to a form target and core kind, and pass through the same surface filtering and entry-key validation as built-in declarations. You do not need a dummy field to attach form-level behaviour.

For React, Vue and Web Components, register trusted definitions with clientRenderedModuleRegistry.register(definition) before mounting the component. The shared host owns reconciliation even when you replace the form component. Framework-independent consumers can pass their own ModuleRegistry to mountClientRenderedModules(root, instance, registry). Declare client-rendered in both the PHP entry and executable definition only when the module supports that component's markup. Keep <FormieForm> for server-rendered forms and <FormieClientForm> for the structured client-rendered product.

Last updated: Sep 30, 2026, 9:21 PM