Daring Designs uses cookies to enhance your experience, analyze site traffic, and improve our services. By clicking dismiss, you agree to our use of cookies.

Skip to main content
Daring Designs
Contact Search

Building a Custom Statamic 6 Fieldtype with Vue 3

A practical guide to building a custom Statamic 6 fieldtype with Vue 3 — the exact vite.config.js pattern, Fieldtype registration, and the gotchas nobody documents.

Statamic 6 ships its control panel on Vue 3. That's great news if you're writing a fieldtype — you get the reactivity API, composition, and modern tooling. But if you've only ever read the docs for Statamic 4 or 5, the addon build setup has changed enough to trip you up. This post walks through the pattern we use on every addon, including the exact vite.config.js that actually works.

Scaffolding the Addon

Statamic ships a CLI for addon scaffolding. From a fresh Statamic 6 install, run this inside an addons directory:

php please make:addon your-vendor/your-addon

That gives you a composer.json, a ServiceProvider, and the Laravel package plumbing. What it does NOT give you is a working Vite config or a fieldtype skeleton — you build those yourself.

The Fieldtype PHP Class

Create a Fieldtype class in your addon's src/Fieldtypes directory:

<?php

namespace YourVendor\YourAddon\Fieldtypes;

use Statamic\Fields\Fieldtype;

class YourField extends Fieldtype
{
    protected static $handle = 'your_field';
    protected static $title = 'Your Field';
    protected $icon = 'code';

    protected function configFieldItems(): array
    {
        return [
            'placeholder' => [
                'type' => 'text',
                'display' => 'Placeholder',
            ],
        ];
    }
}

Register it in your ServiceProvider's $fieldtypes array. Statamic picks it up from there.

The Vue 3 Component

Create resources/js/components/YourField.vue. This is what the control panel renders when an editor encounters your field. The Statamic team exposes a global Fieldtype mixin that handles the value binding for you:

<template>
  <div>
    <input
      type="text"
      :value="value"
      @input="update($event.target.value)"
      :placeholder="config.placeholder"
      class="input-text"
    />
  </div>
</template>

<script>
export default {
  mixins: [Fieldtype],
};
</script>

The `Fieldtype` mixin is provided globally by the CP. It gives you `value`, `config`, and an `update(newValue)` method that syncs back to the parent form.

The Entry Point — Registering the Component

Create resources/js/addon.js. The CP exposes a global `Statamic` object; you hook into it with `Statamic.$components.register(handle, component)`:

import YourField from './components/YourField.vue';

Statamic.booting(() => {
  Statamic.$components.register('your_field-fieldtype', YourField);
});

The component name must match the fieldtype handle suffixed with `-fieldtype` — that's the convention the CP uses to look up your renderer.

The Vite Config (The Part That Actually Matters)

This is where most tutorials lose people. You cannot bundle Vue into your addon — the CP already loads Vue, and shipping a second copy will break reactivity across your components. You also cannot use Vite's default ES module output, because the CP loads addon scripts via a classic <script> tag, not a module loader.

The working recipe is IIFE output + Vue as an external + a small Fieldtype global shim:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: 'resources/dist',
    emptyOutDir: true,
    lib: {
      entry: resolve(__dirname, 'resources/js/addon.js'),
      name: 'YourAddon',
      formats: ['iife'],
      fileName: () => 'addon.js',
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
  resolve: {
    alias: {
      vue: 'vue/dist/vue.esm-bundler.js',
    },
  },
});

Key points: `formats: ['iife']` produces a single self-executing script. `external: ['vue']` tells Rollup not to bundle Vue. The `globals: { vue: 'Vue' }` line tells the IIFE wrapper to resolve `vue` imports from the global `Vue` object that the CP provides.

Wiring the Script Tag

In your ServiceProvider's boot method, tell Statamic to load your compiled script on CP pages:

use Statamic\Statamic;

public function bootAddon(): void
{
    Statamic::script('your-addon', 'addon.js');
}

Statamic looks for the file in resources/dist relative to your addon. Ship the dist folder in your composer package and the CP will serve it.

Gotchas Nobody Documents

  • Options API vs Composition API: The `Fieldtype` mixin is an Options API mixin. If you're using <script setup>, you'll need to inject `value`, `config`, and the update function manually from the parent component instance. For most fieldtypes, sticking with the Options API is the path of least resistance.

  • CSS scoping: Statamic's CP uses Tailwind. If your component uses classes the CP has purged, they won't apply. Either use the CP's existing utility classes, or scope your own CSS inside the component.

  • Hot reload: `npm run dev` won't hot-reload into the CP — the CP doesn't know about your Vite dev server. Run `npm run build -- --watch` instead and refresh the CP manually.

  • Replicator fields: If your fieldtype will be used inside a replicator or Bard set, test that path specifically. Value propagation is subtly different and bugs tend to show up there first.

Wrapping Up

With the scaffolding, Vue component, entry point, vite.config.js, and script registration in place, you have a working custom fieldtype. From there you can add config options, validation, preprocessing/postprocessing on the PHP side, and more elaborate Vue components.

If you're building something niche — a map picker, a color grader, a custom structured input — the fieldtype API is one of the best extension points in the Statamic ecosystem. You get a first-class editing experience without fighting the CMS.