Building a Custom Fieldtype with Vue.js in Statamic

This tutorial shows how to create a custom searchable field in the Statamic 5 Control Panel that fetches data from an API and displays it live.
⚠️ Important: Statamic 5 uses Vue 2, so you must install the Vue 2 plugin for Vite
npm install @vitejs/plugin-vue2✅ Step 1: Setup Vite for the Control Panel
If you're using Vite in your project, you’ll need to include the Control Panel assets to ensure your custom field loads properly.
Update your vite.config.js file:
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import vue2 from "@vitejs/plugin-vue2";
export default defineConfig({
plugins: [
laravel({
input: [
"resources/css/site.css",
"resources/js/site.js",
"resources/css/cp.css",
"resources/js/cp.js",
],
refresh: true,
}),
vue2(),
],
});✅ Step 2: Create the Custom Fieldtype
Run the following command:
php artisan statamic:make:fieldtype ProductPickerThis generates two files:
- app/Fieldtypes/ProductPicker.php
- resources/js/components/fieldtypes/ProductPicker.vue
✅ Step 3: Register the Fieldtype in the Control Panel
Before doing anything else, make sure Statamic can properly load your field in the Control Panel.
Register the Vue component in resources/js/cp.js:
import ProductPicker from "./components/fieldtypes/ProductPicker.vue";
Statamic.booting(() => {
Statamic.$components.register("product_picker-fieldtype", ProductPicker);
});Next, register your Control Panel assets with Statamic to ensure your custom field loads in the Control Panel.
Update your app/Providers/AppServiceProvider.php file:
use Statamic\Statamic;
public function boot()
{
Statamic::vite('app', [
'resources/js/cp.js',
'resources/css/cp.css',
]);
}👉 At this point, your fieldtype is properly wired into the Control Panel.
✅ Step 4: Add Backend Logic to the Fieldtype
Now, update the file: app/Fieldtypes/ProductPicker.php
<?php
namespace App\Fieldtypes;
use Statamic\Fields\Fieldtype;
use Illuminate\Support\Facades\Http;
class ProductPicker extends Fieldtype
{
protected $icon = 'shopping-cart';
public function defaultValue()
{
return null;
}
public function preload()
{
$baseURL = config('services.products.url');
$response = Http::withHeaders([
'Content-Type' => 'application/json',
])->get($baseURL);
if (!$response->ok()) {
throw new \Exception('Failed to fetch data');
}
$data = $response->json();
$options = collect($data)
->map(fn($product) => [
'value' => $product['id'],
'label' => $product['title'] . ' - $' . $product['price'],
])
->values()
->all();
return [
'options' => $options,
'search' => true,
];
}
public function preProcess($data)
{
return $data;
}
public function process($data)
{
return $data;
}
}👉 This fetches products from an API and prepares them for the dropdown.
✅ Step 5: Add Frontend Logic to the Vue Component
Update: resources/js/components/fieldtypes/ProductPicker.vue
<template>
<div class="flex">
<v-select
ref="input"
:input-id="fieldId"
class="flex-1"
append-to-body
:name="name"
:clearable="config.clearable"
:disabled="config.disabled || isReadOnly"
:options="options"
placeholder="Select a product"
:searchable="true"
:multiple="config.multiple"
:value="selectedOptions"
:close-on-select="true"
:create-option="createOption"
@input="onChange"
>
<template #option="{ label }">
<span v-text="label"></span>
</template>
<template #selected-option="{ label }">
<span v-text="label"></span>
</template>
<template #no-options>
<div class="text-sm text-gray-700 py-2 px-4">
No options to choose from.
</div>
</template>
</v-select>
</div>
</template>
<script>
export default {
mixins: [Fieldtype],
computed: {
options() {
return this.meta.options || [];
},
selectedOptions() {
if (this.config.multiple) {
let values = this.value || [];
if (typeof values === "string" || typeof values === "number") {
values = [values];
}
return values.map(
(v) =>
this.options.find((o) => o.value === v) || {
value: v,
label: v,
}
);
}
return this.options.find((o) => o.value === this.value) || null;
},
},
methods: {
onChange(value) {
if (this.config.multiple) {
this.update((value || []).map((v) => v.value));
} else {
this.update(value ? value.value : null);
}
},
createOption(value) {
return { value, label: value };
},
},
};
</script>✅ Step 6: Add the Field to a Blueprint
Add the field to any blueprint where you want to use it:
-
handle: selected_product
field:
type: product_picker
display: 'Product Picker'✅ Step 7: Configure the API
Add this in config/services.php:
'products' => [
'url' => env('PRODUCTS_API'),
],And in your .env:
PRODUCTS_API=https://fakestoreapi.com/products
👉 This is a free public API for testing purposes. You can add your own here.
✅ Step 8: Test It
- Open any entry in the Control Panel.
- You should see a searchable dropdown.
- Selecting a product will save its ID to the field.
🎯 Final Result
You now have:
✅ A custom Statamic fieldtype
✅ Live data fetched from an external API
✅ A searchable dropdown inside the Control Panel


If you want to take this further, the next step would be to add
- debounced API search (instead of preloading everything)
- async loading (for large datasets)
- caching
