Statamic Custom Add-on Development
Statamic add-ons are Laravel packages that extend the CMS through official API: custom Antlers tags, fieldtype fields, modifiers, CP widgets, commands. Distributed via Packagist or used as local packages.
Creating an Add-on
php artisan statamic:make:addon vendor/my-addon
# Creates skeleton in packages/vendor/my-addon/
// packages/vendor/my-addon/composer.json
{
"name": "vendor/my-addon",
"description": "My Statamic Addon",
"type": "statamic-addon",
"require": {
"statamic/cms": "^4.0"
},
"extra": {
"statamic": {
"name": "My Addon",
"slug": "my-addon"
}
},
"autoload": {
"psr-4": { "Vendor\\MyAddon\\": "src/" }
}
}
// composer.json project — connecting local package
"repositories": [
{ "type": "path", "url": "packages/vendor/my-addon" }
],
ServiceProvider — component registration
// src/ServiceProvider.php
namespace Vendor\MyAddon;
use Statamic\Providers\AddonServiceProvider;
use Statamic\Facades\Fieldtype;
use Statamic\Facades\Modifier;
class ServiceProvider extends AddonServiceProvider
{
protected $tags = [
\Vendor\MyAddon\Tags\SocialShare::class,
\Vendor\MyAddon\Tags\RelatedContent::class,
];
protected $fieldtypes = [
\Vendor\MyAddon\Fieldtypes\ColorSwatchFieldtype::class,
];
protected $modifiers = [
\Vendor\MyAddon\Modifiers\ReadingTime::class,
\Vendor\MyAddon\Modifiers\Truncate::class,
];
protected $widgets = [
\Vendor\MyAddon\Widgets\RecentEditsWidget::class,
];
protected $commands = [
\Vendor\MyAddon\Console\Commands\ImportContent::class,
];
public function boot(): void
{
parent::boot();
// Publish configs
$this->mergeConfigFrom(__DIR__.'/../config/my-addon.php', 'my-addon');
$this->publishes([
__DIR__.'/../config/my-addon.php' => config_path('my-addon.php'),
], 'my-addon-config');
// Load views
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-addon');
}
}
Custom Antlers Tag
// src/Tags/SocialShare.php
namespace Vendor\MyAddon\Tags;
use Statamic\Tags\Tags;
class SocialShare extends Tags
{
protected static $handle = 'social_share';
/**
* {{ social_share url="{url}" title="{title}" platform="twitter" }}
*/
public function index(): string
{
$url = urlencode($this->params->get('url', request()->url()));
$title = urlencode($this->params->get('title', ''));
$platform = $this->params->get('platform', 'all');
return match ($platform) {
'twitter' => "https://twitter.com/intent/tweet?url={$url}&text={$title}",
'telegram' => "https://t.me/share/url?url={$url}&text={$title}",
'vk' => "https://vk.com/share.php?url={$url}&title={$title}",
default => $this->renderAllButtons($url, $title),
};
}
/**
* {{ social_share:buttons url="{url}" }}
* Renders view with buttons
*/
public function buttons(): string
{
return view('my-addon::social-share', [
'url' => urlencode($this->params->get('url', request()->url())),
'title' => urlencode($this->params->get('title', '')),
])->render();
}
}
Usage in Antlers:
{{ social_share:buttons url="{url}" title="{title}" }}
Custom Fieldtype
// src/Fieldtypes/ColorSwatchFieldtype.php
namespace Vendor\MyAddon\Fieldtypes;
use Statamic\Fields\Fieldtype;
class ColorSwatchFieldtype extends Fieldtype
{
protected static $handle = 'color_swatch';
public static function title(): string
{
return 'Color Swatch';
}
public function configFieldItems(): array
{
return [
'swatches' => [
'display' => 'Color Swatches',
'type' => 'array',
'value_header' => 'HEX Value',
'key_header' => 'Name',
],
];
}
public function preload(): array
{
return [
'swatches' => $this->config('swatches', []),
];
}
public function preProcess(mixed $data): mixed
{
return $data ?? null;
}
public function process(mixed $data): mixed
{
return $data;
}
}
Vue component for CP (resources/js/components/fieldtypes/ColorSwatchFieldtype.vue):
<template>
<div class="color-swatches">
<div
v-for="(hex, name) in meta.swatches"
:key="name"
class="swatch"
:class="{ selected: value === hex }"
:style="{ backgroundColor: hex }"
:title="name"
@click="$emit('input', hex)"
/>
<div v-if="value" class="selected-color">
{{ value }}
<button @click="$emit('input', null)">×</button>
</div>
</div>
</template>
Antlers Modifier
// src/Modifiers/ReadingTime.php
namespace Vendor\MyAddon\Modifiers;
use Statamic\Modifiers\Modifier;
class ReadingTime extends Modifier
{
protected static $handle = 'reading_time';
public function index(string $value, array $params): string
{
$wordsPerMinute = (int) ($params[0] ?? 200);
$wordCount = str_word_count(strip_tags($value));
$minutes = (int) ceil($wordCount / $wordsPerMinute);
return $minutes . ' min read';
}
}
{{ content | reading_time:200 }}
Testing Add-on
// tests/Unit/Tags/SocialShareTest.php
use Vendor\MyAddon\Tests\TestCase;
class SocialShareTest extends TestCase
{
/** @test */
public function it_generates_twitter_share_url()
{
$result = $this->tag('social_share')
->params(['url' => 'https://example.com', 'platform' => 'twitter'])
->fetch();
$this->assertStringContainsString('twitter.com/intent/tweet', $result);
$this->assertStringContainsString(urlencode('https://example.com'), $result);
}
}
Development Timeline
| Type | Time |
|---|---|
| 2–3 Antlers tags | 1–2 days |
| Fieldtype with Vue component | 2–4 days |
| CP widget | 1–2 days |
| Full add-on (tags + fieldtype + settings) | 1–2 weeks |
| Marketplace publication prep | +1–2 days |







