Skip to main content
SlugField ← Back to Table of Contents

Summary

Permalink field for Filament: title + slug in one block, live URL preview, inline editing, Copy/Visit/Regenerate buttons, and uniqueness validation.
Spatie laravel-sluggable is optional. By default, the slug is generated from the title using Str::slug() in the browser and saved to the database like a regular form field. You only need to add the Spatie package if you want the same rules as on model saving (such as -2, -3 suffixes, preventOverwrite, etc.).
Slug field classBjanczak\FilamentFlexFields\Filament\Forms\Components\SlugField
Title + slug factoryBjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField
Convenience schemaSlugField::withTitle() → returns a FusedGroup (same as TitleSlugField::make())
State typestring|null (normalized slug; homepage slug is '/' when enabled)
FieldTypeslug
SpatieOptional — see Spatie laravel-sluggable integration

Start here — integration without Spatie (default)

No extra packages required besides filament-flex-fields. The model does not need any traits or slug options—just a database column and $fillable configuration.

Who is responsible for what

WhatWho does itDo you need to write code?
Slug preview while typing the titleSlugField (Alpine + Str::slug)No — works automatically
Saving the slug value to the databaseFilament + EloquentYes — database column + $fillable
Slug uniqueness in the formSlugField (unique rule)No — enabled by default
Suffix -2 on database collisionOnly Spatie (HasSlug)No — without Spatie, the slug must be unique in the form
Create vs Edit behaviourTitleSlugFieldNo — by default, the slug does not change on edit

Checklist — 4 steps

1. Migration     → columns: title + slug (slug usually unique)
2. Model        → slug in $fillable (NO HasSlug, NO Spatie)
3. Form         → TitleSlugField::make()
4. Config       → optional url_host in config (for UI permalink)

Step 1 — Migration

Schema::create('posts', function (Blueprint $table): void {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();  // required: slug column
    $table->timestamps();
});

Step 2 — Model (minimal, without Spatie)

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /** @var list<string> */
    protected $fillable = [
        'title',
        'slug',   // necessary — otherwise Filament won't save the slug
    ];
}
The model does not need:
  • use HasSlug ani getSlugOptions()
  • an observer generating the slug
  • mutatora setSlugAttribute
  • composer require spatie/laravel-sluggable
The slug is saved to the record just like title—from the form data.

Step 3 — Filament Resource (minimum)

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TitleSlugField::make(),   // that's it. No parameters required.
        // ...pozostałe pola
    ]);
}
This creates: a title field + a hidden auto-sync flag + a slug field in one FusedGroup. If you want to see https://your-domain.com/blog/my-post under the slug:
// config/filament-flex-fields.php (after publishing config)
'slug' => [
    'field_title' => 'title',      // title form field name
    'field_slug' => 'slug',        // slug form field name
    'url_host' => env('APP_URL'),  // null = no full URL preview bar
],
Or only in the Resource, without changing the config:
TitleSlugField::make(
    urlHost: config('app.url'),
    urlPath: '/blog/',
),

Parameters of TitleSlugField::make() — what is available?

ParameterRequired?DefaultDescription
(żaden)TitleSlugField::make() wystarczy na start
fieldTitleNie'title'Alternative title column/field name
fieldSlugNie'slug'Alternative slug column/field name
urlHostNiez config lub nullFull URL preview host
urlPathNienullPath prefix, e.g. /blog/
preserveSlugOnEditNietruefalse = slug always syncs with title
translatableLocalesNiez config lub nullEnables multi-language tabs (TranslatableFields)
slugSourceLocaleNieapp.locale / pierwszy localeWhich title language drives slug generation
requiredTitleLocalesNieonly slugSourceLocale'all', ['en'] lub null — which title locales are required
spatieTranslatableNiefalseConfig flag for Spatie models — see Translatable titles
titleLocaleConfiguratorNienullfn (FlexTextInput $field, string $locale) =&gt; $field
translatableFieldsConfiguratorNienullfn (TranslatableFields $fields) =&gt; $fields-&gt;… — custom configuration of title tabs
spatieModelNienullOnly for Spatie Sluggable (HasSlug) — do not confuse with Translatable

What happens automatically (without Spatie)

EventBehaviour
Create — user types titleSlug updates live (Hello Worldhello-world)
Edit — user changes titleSlug does not change (preserves published URL)
Edit — manual slug editAuto-sync turns off; badge shows Custom; Regenerate appears
SaveForm slug value → slug database column
Duplicate slugForm validation error (before database write)

Slug generation without Spatie (technical)

Title (live) → debounce 400ms → Str::slug() → normalizeSlug() → slug field
No server requests. No model configuration required.

Four ways to add title + slug

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\SlugField;
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;

// 1) Recommended — one liner
TitleSlugField::make(),

// 2) Same layout, different import
SlugField::withTitle(),

// 3) Manual — full control (title embedded in slug field)
SlugField::make('slug')
    ->source('title')
    ->titleField(FlexTextInput::make('title')->required()),

// 4) Title elsewhere in the schema — slug syncs via ->source() (playground: slug__standalone)
FlexTextInput::make('title')->label('Title')->live(),
SlugField::make('slug')
    ->label('Slug')
    ->source('title')
    ->helperText('Auto-syncs from title until you edit or reset the slug.'),

When to switch to Spatie?

Only add Spatie when you need model-level hooks that the form alone cannot handle:
  • automatic -2, -3 suffixes on database collisions
  • preventOverwrite — nigdy nie nadpisuj sluga po publikacji
  • skipGenerateWhen, extraScope, wiele pól źródłowych
  • using the same SlugOptions in form preview and on save()
Do tego: Spatie laravel-sluggable integration.

Common issues (without Spatie)

SymptomCauseFix
Slug does not saveMissing slug in $fillableAdd to $fillable
No URL preview under slugurl_host is nullSet APP_URL in .env or -&gt;urlHost(...)
Slug does not update from titleManual edit disabled auto-syncClick Regenerate
Validation: slug already existsDuplicate in databaseChange slug or delete the old record
name / handle fields instead of title/slugDefault field namesfieldTitle: / fieldSlug: lub config
Next sections: Default form layoutInstallationConfigFull ExampleSpatie Integration

Default form layout (FusedGroup)

TitleSlugField::make() zawsze zwraca Filament\Schemas\Components\FusedGroup — ten sam układ z parametrami lub bez:
// Bez Spatie (domyślna ścieżka)
TitleSlugField::make(),

// Z permalinkiem (nadal bez Spatie)
TitleSlugField::make(
    urlHost: config('app.url'),
    urlPath: '/blog/',
),

// With Spatie — identical layout, different preview generation logic
TitleSlugField::make(spatieModel: Post::class),
Important: spatieModel changes only the slug preview generation logic (server + SlugOptions). It does not change the form layout.

Co jest wewnątrz FusedGroup

#KomponentState path (domyślnie)Widoczny?Rola
1FlexTextInputtitleTakPole tytułu, live(), auto-sync do sluga
2Hiddenslug_auto_update_disabledNieFlaga: użytkownik ręcznie edytował slug
3SlugFieldslugTakPermalink, inline edit, akcje Copy/Visit/…
The group has CSS class fff-title-slug-fused-group (without the standard Filament border between fields).

Domyślny wygląd (ASCII)

When config('filament-flex-fields.slug.url_host') is set (e.g. APP_URL):
┌─ Title (FlexTextInput) ─────────────────────────────────────┐
│  Label: "Title"                                              │
│  [ Luxury Yacht Charter in the Mediterranean          ]      │
└──────────────────────────────────────────────────────────────┘

┌─ Slug (SlugField) — etykieta ukryta ────────────────────────┐
│  Permalink                                    [ Auto ]       │
│  🔒 wyachts.test/charters/luxury-yacht-charter              │
│                                                              │
│  [ Edit ]                    [ Regenerate ] [ Copy ] [ Visit ]│
└──────────────────────────────────────────────────────────────┘
When url_host is null (no URL preview in config):
┌─ Title ─────────────────────────────────────────────────────┐
│  Label: "Title"                                              │
│  [ My post title                                        ]    │
└──────────────────────────────────────────────────────────────┘

┌─ Slug — tryb inline edit ───────────────────────────────────┐
│  wyachts.test/charters/my-post-title   (preview + Edit)      │
│  [ Edit ]                              [ Copy ]              │
└──────────────────────────────────────────────────────────────┘

Table of default visual values

ElementDomyślna wartośćSkąd się bierze
Title field nametitleconfig('filament-flex-fields.slug.field_title')
Slug field nameslugconfig('filament-flex-fields.slug.field_slug')
Label title"Title"Str::headline($fieldTitle)
Placeholder title"Title"j.w.
Label slugukrytyslugLabel: nullhiddenLabel()
Slug sizemd (40px)config('filament-flex-fields.ui.slug_size')
Slug variantprimaryconfig('filament-flex-fields.ui.slug_variant')
Permalink hostAPP_URL lub nullconfig('filament-flex-fields.slug.url_host')
Etykiety przyciskówtekst + ikonaconfig('filament-flex-fields.slug.action_button_labels')
IkonyGravity UInp. gravityui-pencil, gravityui-copy
BadgeAuto / CustomAlpine — po ręcznej edycji sluga

The same layout — three ways to call

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\SlugField;

// A) Factory (recommended) — without Spatie
TitleSlugField::make(),

// B) Alias — identical FusedGroup
SlugField::withTitle(),

// C) More options via slugConfigurator (e.g. permalink)
TitleSlugField::make(
    slugConfigurator: fn (SlugField $slug) => $slug
        ->urlHost(config('app.url'))
        ->urlPath('/blog/'),
),
Livewire test helper — hidden auto-sync field name:
TitleSlugField::autoUpdateDisabledFieldName('slug');    // slug_auto_update_disabled
TitleSlugField::autoUpdateDisabledFieldName('permalink'); // permalink_auto_update_disabled

Instalacja i assety

Pakiet jest częścią janczakb/filament-flex-fields. Ścieżka bez Spatie wymaga tylko assetów pakietu — bez dodatkowych composer require.
# In the package / application directory
npm run build:js:slug-field
npm run build:css
php artisan filament:assets

Opcjonalnie: Spatie (dopiero gdy potrzebujesz)

Instaluj tylko jeśli model ma HasSlug / getSlugOptions() i chcesz zgodny podgląd w formularzu:
composer require spatie/laravel-sluggable
Bez tego pakietu TitleSlugField::make() działa w pełni — generowanie przez Str::slug() w przeglądarce.

Konfiguracja pakietu (config/filament-flex-fields.php)

Opublikuj config:
php artisan vendor:publish --tag=filament-flex-fields-config
Klucze dotyczące sluga:
// config/filament-flex-fields.php
return [
    'slug' => [
        // Default field names pól w TitleSlugField::make()
        'field_title' => 'title',
        'field_slug' => 'slug',

        // Host w pasku permalink (null = brak paska hosta)
        'url_host' => env('APP_URL'),

        // true = przyciski z tekstem; false = same ikony + tooltip
        'action_button_labels' => true,
    ],

    'ui' => [
        'slug_size' => 'md',      // sm | md | lg
        'slug_variant' => 'primary', // primary | secondary | …
    ],
];
Przykład — blog z polskimi nazwami pól:
// config/filament-flex-fields.php
'slug' => [
    'field_title' => 'tytul',
    'field_slug' => 'adres',
    'url_host' => 'https://mojblog.pl',
],
// W Resource — bez podawania nazw pól
TitleSlugField::make(
    titleLabel: 'Tytuł wpisu',
    urlPath: '/wpisy/',
),

Full Example from scratch (migration -> model -> Resource)

Continuation of the Start here — integration without Spatie section. Steps 1–3 are the minimum; step 4 (Spatie) is optional.

1. Migracja

Schema::create('posts', function (Blueprint $table): void {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body')->nullable();
    $table->timestamps();
});

2. Model (bez Spatie — wystarczy na produkcję)

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'slug', 'body'];
}
Nie dodawaj HasSlug ani getSlugOptions() — chyba że przechodzisz na krok 4 poniżej.

3. Filament Resource (without Spatie)

namespace App\Filament\Resources\Posts\Schemas;

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;
use Filament\Forms\Components\RichEditor;
use Filament\Schemas\Schema;

class PostForm
{
    public static function configure(Schema $schema): Schema
    {
        return $schema->components([
            TitleSlugField::make(
                urlHost: config('app.url'),
                urlPath: '/blog/',
            ),

            RichEditor::make('body'),
        ]);
    }
}

4. (Optional) The same Resource with Spatie

Add this step only when you need suffixes, preventOverwrite, or other SlugOptions rules during save.
// app/Models/Post.php
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;

class Post extends Model
{
    use HasSlug;

    public function getSlugOptions(): SlugOptions
    {
        return SlugOptions::create()
            ->generateSlugsFrom('title')
            ->saveSlugsTo('slug');
    }
}
// Formularz — wygląd identyczny, slug preview zgodny z modelem
TitleSlugField::make(
    spatieModel: Post::class,
    urlHost: config('app.url'),
    urlPath: '/blog/',
),
Dwie warstwy: formularz pokazuje podgląd sluga na żywo; przy zapisie rekordu Spatie HasSlug może dodać suffix (-2) lub zastosować preventOverwrite — to normalne.

Cookbook — typical scenarios

Scenario 1: Blog — create + edit (default behaviour)

TitleSlugField::make(
    urlHost: config('app.url'),
    urlPath: '/posts/',
),
  • Create: tytuł → slug na żywo.
  • Edit: zmiana tytułu nie zmienia sluga.

Scenario 2: Always sync slug with title

TitleSlugField::make(
    preserveSlugOnEdit: false,
    urlHost: config('app.url'),
),

Scenario 3: Slug read-only on edit

TitleSlugField::make(
    slugConfigurator: fn (SlugField $field) => $field
        ->slugReadOnly(fn (SlugField $component): bool => $component->getOperation() === 'edit'),
),

Scenario 4: Custom title (RichEditor) + slug

use Filament\Forms\Components\RichEditor;

TitleSlugField::make(
    titleField: RichEditor::make('title')
        ->required()
        ->columnSpanFull(),
    slugConfigurator: fn (SlugField $slug) => $slug
        ->urlHost(config('app.url'))
        ->urlPath('/news/'),
),

Scenario 5: Slug uniqueness within tenant

TitleSlugField::make(
    slugConfigurator: fn (SlugField $slug) => $slug
        ->slugUniqueModel(Post::class)
        ->slugUniqueScope(fn ($query) => $query->where('tenant_id', filament()->getTenant()->id)),
),
TitleSlugField::make(
    urlHost: null,
    slugConfigurator: fn (SlugField $slug) => $slug
        ->permalinkPreview(false)
        ->inlineEditing(false),
),

Scenario 7: CMS Homepage (/)

Standalone SlugField (matches playground Homepage slug):
SlugField::make('slug')
    ->label('Homepage slug')
    ->allowHomepageSlug()
    ->urlHost('https://wyachts.test')
    ->slugPattern('/^(\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/')
    ->helperText('Supports "/" as homepage slug.'),
Inside TitleSlugField:
TitleSlugField::make(
    slugConfigurator: fn (SlugField $slug) => $slug
        ->allowHomepageSlug()
        ->slugPattern('/^(\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/'),
),

Scenario 8: Repeater — row with title and slug

use Filament\Forms\Components\Repeater;

Repeater::make('sections')
    ->schema([
        FlexTextInput::make('title')->required()->live(),
        SlugField::make('slug')
            ->source('title')
            ->urlHost(config('app.url'))
            ->urlPath('/docs/'),
    ])
    ->columns(1),
Ścieżki zagnieżdżone (sections.0.titlesections.0.slug) są rozwiązywane automatycznie.

Scenario 9: Manual slug only (no title, no auto-generate)

Use when there is no title field — user types the slug by hand. This is not the same as playground slug__standalone (that name means “slug field alone in the layout”, but it still uses ->source('title')).
SlugField::make('slug')
    ->label('URL slug')
    ->autoGenerate(false)
    ->inlineEditing(false)
    ->urlHost(config('app.url'))
    ->urlPath('/posts/')
    ->required(),

Scenario 10: Form read-only (whole field)

Matches playground Form readonly:
SlugField::make('slug')
    ->label('Form readonly')
    ->urlHost('https://wyachts.test')
    ->urlPath('/docs/')
    ->readOnly(),

Scenario 11: Spatie + multiple source fields (optional package)

// Model
public function getSlugOptions(): SlugOptions
{
    return SlugOptions::create()
        ->generateSlugsFrom(['title', 'subtitle'])
        ->saveSlugsTo('slug');
}

// Formularz
TitleSlugField::make(spatieModel: Post::class),
FlexTextInput::make('subtitle')->live(),

Quick Start — Filament Resource (create + edit)

Summary of the Start here section — without Spatie:
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;

public static function form(Schema $schema): Schema
{
    return $schema->components([
        TitleSlugField::make(
            urlHost: config('app.url'),  // opcjonalne — permalink w UI
            urlPath: '/blog/',           // opcjonalne — prefix ścieżki
        ),
        // ...other fields
    ]);
}
Application-side requirements: slug column in migration + slug in model’s $fillable. Nothing more. What happens automatically:
EventBehaviour
User types title on createSlug updates live (hello-world)
User opens editExisting slug is preserved when title changes
User clicks Edit on slug and saves a custom valueAuto-sync stops; badge shows Custom
User clicks Regenerate (after manual edit)Slug is rebuilt from current title
Duplicate slug on saveValidation error (unique rule)

How slug generation works

Title input (live)


Debounce (generationDebounce, default 400ms)

       ├── slugifyUsing() set? ──► your Closure

       ├── spatieModel() + Spatie installed? ──► SpatieSlugIntegration
       │         (GenerateSlugAction, SlugOptions, #[Sluggable] attribute)

       └── else ──► SlugGenerator::fromString() (Str::slug + normalize)


normalizeSlug() ──► form state + validation
Form preview vs model save:
LayerResponsibility
SlugField (browser / optional server preview)Shows what the slug will look like while typing
Eloquent model + Spatie HasSlugFinal slug on create / update (unique suffix, preventOverwrite, etc.)
When Spatie is configured, the field uses the same SlugOptions as your model so previews match production rules (separator, language, max length, multi-field sources, extraScope, suffix start, Closure sources).

Create vs edit

Default (preserve slug on edit)

TitleSlugField::make(
    preserveSlugOnEdit: true, // default
),
On edit, changing the title does not change the slug. Good for published URLs.

Always sync slug from title

TitleSlugField::make(
    preserveSlugOnEdit: false,
),

Read-only slug on edit only

TitleSlugField::make(
    slugReadOnly: fn (): bool => true, // always
),

// Or only on edit (Filament operation):
TitleSlugField::make(
    slugConfigurator: fn (SlugField $field) => $field
        ->slugReadOnly(fn (SlugField $component): bool => $component->getOperation() === 'edit'),
),

Read-only title

TitleSlugField::make(
    titleReadOnly: true,
),

Universal locale tabs: For any translatable attribute (title, body, metadata, …), use the dedicated TranslatableFields component. The section below covers TitleSlugField only — translatable titles with a single shared slug.

Translatable titles (single slug)

For generic translatable fields (body, excerpt, metadata, …) without slug coupling, use TranslatableFields instead.
Optional multi-language title with one shared slug. Locale switching uses package TranslatableFields (built on SegmentTabs).
LayerBehaviour
Title form stateDot paths: title.pl, title.en, …
Title DB stateNested array / JSON: {"pl":"…","en":"…"}
Slug form + DBSingle string — not translatable
Auto-syncOnly when the source locale tab changes
Required titleBy default, only the source locale is required()

What is implemented today

FeatureStatus
TranslatableFields per localeYes
translatableFieldsConfigurator passthroughYes — RTL, badges, modifyFieldsUsing(), Spatie flag, etc.
slugSourceLocale — pick slug source languageYes
Works without Spatie (array / json cast)Yes
Hydrate edit form from JSON columnYes
Auto-detect HasTranslations on record (when package installed)Yes — getTranslation($attribute, $locale, false) per tab
spatieTranslatable: true config flagYes — documents intent for FlexField / config
Separate Spatie package dependency in this packageNo — optional composer require spatie/laravel-translatable
Per-locale slugNo — one slug by design
Spatie locale fallback in form tabsNo — each tab shows that locale only
Deep runtime bridge like laravel-sluggableNo — compatible state shape + hydrate/save, not a full plugin adapter
spatieModel ≠ Spatie Translatable. spatieModel on TitleSlugField is for Spatie Sluggable (HasSlug). For translations use translatableLocales (+ optional spatieTranslatable).

Basic usage

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;

TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN', 'fr' => 'FR'],
    slugSourceLocale: 'pl',
    urlHost: config('app.url'),
    urlPath: '/pages/',
);
Changing EN/FR titles does not change the slug. Only the slugSourceLocale tab drives permalink generation.
class Page extends Model
{
    protected $fillable = ['title', 'slug'];

    protected function casts(): array
    {
        return [
            'title' => 'array',
        ];
    }
}
// migration
$table->json('title');
$table->string('slug')->unique();
On save, Filament merges title.pl / title.en into a title array. No extra glue code required.

Storage with Spatie laravel-translatable (optional)

composer require spatie/laravel-translatable
use Spatie\Translatable\HasTranslations;

class Page extends Model
{
    use HasTranslations;

    public array $translatable = ['title'];

    protected $fillable = ['title', 'slug'];
    // slug is intentionally NOT in $translatable
}
TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN'],
    slugSourceLocale: 'pl',
    spatieTranslatable: true, // optional config marker
);
On edit: if the record uses HasTranslations and the package is installed, each tab is hydrated via getTranslation(). Otherwise tabs read the raw JSON attribute. On save: the nested title array from the form is assigned to the model; Spatie JSON-encodes translatable attributes automatically.

Global defaults (config/filament-flex-fields.php)

'slug' => [
    'translatable_locales' => ['pl' => 'PL', 'en' => 'EN'],
    'slug_source_locale' => 'pl',
    'spatie_translatable' => false,
    'required_title_locales' => null, // null | 'all' | ['en']
],
When translatableLocales is omitted in TitleSlugField::make(), locales are read from config('filament-flex-fields.slug.translatable_locales').

Required title locales

By default only the slug source locale title is required. Optional locales can stay empty on create/edit.
// Default — only slugSourceLocale required (PL required, EN optional)
TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN'],
    slugSourceLocale: 'pl',
);

// All locales required
TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN'],
    requiredTitleLocales: 'all',
);

// Only specific locales required
TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN'],
    requiredTitleLocales: ['en'],
);
FlexField config key: required_title_locales (null, 'all', or list of locale codes).

Per-locale title customization

TitleSlugField::make(
    translatableLocales: ['pl', 'en'],
    slugSourceLocale: 'pl',
    titleLocaleConfigurator: fn (FlexTextInput $field, string $locale) => $field
        ->placeholder(match ($locale) {
            'pl' => 'Title in Polish',
            'en' => 'Title in English',
            default => 'Title',
        }),
);

Full TranslatableFields passthrough

When translatableLocales is set, title tabs are built with TranslatableFields internally. By default the factory enables directionByLocale() and emptyBadgeWhenAllFieldsAreEmpty() (warning empty badge on tabs where the title is blank). The active tab stays on slugSourceLocale, not activeTabWithValue(). Use translatableFieldsConfigurator for further tweaks (activeTabWithValue(), bordered panels, custom tab icons, localeFieldUsing(), storageAttributeUsing(), etc.):
use Bjanczak\FilamentFlexFields\Filament\Schemas\Components\TranslatableFields;

TitleSlugField::make(
    translatableLocales: ['pl' => 'PL', 'en' => 'EN', 'ar' => 'AR'],
    slugSourceLocale: 'pl',
    translatableFieldsConfigurator: fn (TranslatableFields $fields): TranslatableFields => $fields
        ->activeTabWithValue()
        ->modifyTabsUsing(fn ($tab, string $locale) => $tab->icon('heroicon-o-language')),
),

Per-locale field tweaks remain available via `titleLocaleConfigurator` (applied after default title field setup).

#### Standalone `SlugField` with translatable source

```php
SlugField::make('slug')
    ->translatableTitle()
    ->titleLocales(['pl' => 'PL', 'en' => 'EN'])
    ->slugSourceLocale('pl')
    ->translatableTitleField('title');
getSourceStatePath() resolves to title.{slugSourceLocale} automatically (e.g. title.pl).

SlugField translatable API

MethodDescription
translatableTitle(bool|Closure $condition = true)Enable translatable source resolution
titleLocales(array|Closure $locales)Locale map or list — also enables translatable mode
slugSourceLocale(string|Closure $locale)Locale used for slug generation
translatableTitleField(string|Closure $fieldName)Base title attribute name (default: title)
spatieTranslatable(bool|Closure $condition = true)Config flag (FlexField schema); hydrate auto-detects Spatie when present
usesTranslatableTitle()Whether translatable mode is active
getTitleLocales()Resolved locale =&gt; label map
getSlugSourceLocale()Effective source locale
shouldUseSpatieTranslatable()Evaluated spatieTranslatable flag

FlexField schema config

Config keyMaps to
translatable_localesTitleSlugField::make(translatableLocales: …) / titleLocales()
slug_source_localeslugSourceLocale()
required_title_localesrequiredTitleLocales / TitleSlugField::make(requiredTitleLocales: …)
spatie_translatablespatieTranslatable()
translatable_title_fieldtranslatableTitleField()

Slug generation locale

Translatable titles always use server-side slug preview (generateSlugPreview / Str::slug with slugSourceLocale). Alpine receives serverGenerate: true and slugSourceLocale so live preview matches PHP — including Polish diacritics (Łódźlodz), which generic browser ASCII folding cannot handle reliably.

Spatie laravel-sluggable integration (v4.x)

Not required. If the default path without Spatie is sufficient, you can skip this section entirely.
Tested with spatie/laravel-sluggable ^4.0 (currently 4.0.2). The integration uses official Spatie v4 classes:
  • Spatie\Sluggable\Actions\GenerateSlugAction (via config/sluggable.phpactions.generate_slug)
  • Spatie\Sluggable\Support\SluggableAttributeResolver dla modeli z #[Sluggable] bez getSlugOptions()
  • Spatie\Sluggable\Support\Config::getAction() — ten sam resolver akcji co trait HasSlug
Spatie adds a second layer: saving the slug with model options (suffixes, scope, preventOverwrite). The form can display the same preview as the model — just pass spatieModel.

Optional dependency isolation (technical)

WarstwaImport Spatie\…?Bez composer require spatie/laravel-sluggable
SlugField, TitleSlugFieldNoWorks 100% (SlugGenerator, permalink, unique, inline edit)
Traits GeneratesSlugFromSource, ConfiguresSlugPermalinkNo — only SpatieSlugIntegration::isAvailable()Guard before each Spatie call
Support/Slug/SpatieSlugIntegration.phpYes — the only slug bridgeLoaded safely; isAvailable() returns false, falls back to SlugGenerator
spatie/laravel-sluggable jest w composer.jsonsuggest, nie w require. Pakiet nie wymusza instalacji Spatie. Optional. Install when you want model-driven slug rules:
composer require spatie/laravel-sluggable

Minimal model (trait — klasycznie)

use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;

class Post extends Model
{
    use HasSlug;

    public function getSlugOptions(): SlugOptions
    {
        return SlugOptions::create()
            ->generateSlugsFrom('title')
            ->saveSlugsTo('slug');
    }
}

Minimal model (v4 attribute — bez getSlugOptions())

use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\Attributes\Sluggable;

#[Sluggable(from: 'title', to: 'slug')]
class Post extends Model
{
    // Brak HasSlug — SlugField odczytuje opcje przez SluggableAttributeResolver
}
Multi-field attribute (v4):
#[Sluggable(from: ['title', 'subtitle'], to: 'slug')]
class Post extends Model {}

Wire the form (zero extra config)

Spatie in the form is enabled only when:
  • podasz spatieModel, i
  • model ma getSlugOptions() lub atrybut #[Sluggable] z rozpoznawalnymi opcjami.
Simply having Post as the Resource model does not enable Spatie automatically — the slug configuration must exist on the model.
// Form layout: IDENTICAL FusedGroup in both cases
TitleSlugField::make(),

// Jawne wskazanie modelu Spatie (zalecane gdy Resource nie binduje modelu)
TitleSlugField::make(spatieModel: Post::class),
What changes after adding spatieModel:
AspektBez SpatieZe spatieModel
UI Layout (FusedGroup)SameSame
Preview generationStr::slug in JS or SlugGeneratorGenerateSlugAction + SlugOptions
Requesty LivewireOpcjonalneTak (generateSlugPreview)
Zapis do bazyTwoja logika / FilamentSpatie HasSlug na modelu
Or on standalone SlugField:
SlugField::make('slug')
    ->source('title')
    ->spatieModel(Post::class),

Explicit Spatie field mapping

When form field names differ from model attributes:
SlugField::make('permalink')
    ->source('name')
    ->spatieModel(Post::class)
    ->spatieSlugField('slug')      // model column Spatie writes to
    ->spatieSourceField('title'),  // model attribute used as primary source

Supported Spatie SlugOptions features in preview

Spatie optionSupported in live preview
generateSlugsFrom('title')Yes
generateSlugsFrom(['title', 'subtitle'])Yes — reads sibling form fields
generateSlugsFrom(fn ($model) =&gt; ...)Yes — Closure receives hydrated model
saveSlugsTo()Yes — via spatieSlugField()
usingSeparator() / usingLanguage()Yes
slugsShouldBeNoLongerThan()Yes
generateUniqueSlugs / suffixYes — queries DB for collisions
extraScope()Yes — hydrates model from full form state (data.*, hidden fields, fillable attributes)
startSlugSuffixFrom() / useSuffixOnFirstOccurrence()Yes
usingSuffixGenerator()Yes
skipGenerateWhen()Yes — keeps existing slug in preview; reads hydrated form state
preventOverwrite()Yes — keeps existing slug in preview
#[Sluggable] attribute (without getSlugOptions())Yes — via SluggableAttributeResolver (v4)
#[Sluggable(from: ['title', 'subtitle'])Yes — sibling form fields
HasTranslatableSlug + spatie/laravel-translatableYes — preview for slugSourceLocale / app.locale
selfHealing() / route keysYes — permalink preview and Visit URL append {slug}{separator}{id} on edit
doNotGenerateSlugsOnCreate() / OnUpdate()Model save only — preview always generates
Custom GenerateSlugActionYes — via config/sluggable.php

Multi-field source example

// Model
public function getSlugOptions(): SlugOptions
{
    return SlugOptions::create()
        ->generateSlugsFrom(['title', 'subtitle'])
        ->saveSlugsTo('slug');
}

// Form — include both fields; slug preview concatenates them
TitleSlugField::make(spatieModel: Post::class),
FlexTextInput::make('subtitle')->live(),
The form must have the fields used in extraScope / skipGenerateWhen (e.g., tenant_id, status) filled out — SlugField reads them from the live form state (data.*), not just the source fields.

Scoped unique slugs (Spatie extraScope)

public function getSlugOptions(): SlugOptions
{
    return SlugOptions::create()
        ->generateSlugsFrom('title')
        ->saveSlugsTo('slug')
        ->extraScope(fn ($query) => $query->where('tenant_id', $this->tenant_id));
}

Attribute-based model (Spatie v4+)

use Spatie\Sluggable\Attributes\Sluggable;
use Spatie\Sluggable\HasSlug;

#[Sluggable(from: 'title', to: 'slug', separator: '-', unique: true)]
class Post extends Model
{
    use HasSlug;
}
No getSlugOptions() required — SlugField reads the attribute when the method is absent.

Override Spatie for preview only

SlugField::make('slug')
    ->source('title')
    ->spatieModel(Post::class)
    ->slugifyUsing(fn (array $state): string => strtoupper($state['source'])),
slugifyUsing() always wins over Spatie.

Force server-side preview

Spatie mode and translatable titles already use server-side generateSlugPreview. For custom slugifiers or other cases:
SlugField::make('slug')
    ->source('title')
    ->serverSideGeneration(),

Custom Spatie action class

If you override config/sluggable.phpactions.generate_slug, the field uses your bound GenerateSlugAction implementation automatically.

skipGenerateWhen — preview without overwriting

public function getSlugOptions(): SlugOptions
{
    return SlugOptions::create()
        ->generateSlugsFrom('title')
        ->saveSlugsTo('slug')
        ->skipGenerateWhen(fn (): bool => $this->status === 'published');
}
In the form preview, when skipGenerateWhen returns true, the field will keep the existing slug instead of generating a new one.

startSlugSuffixFrom and collisions in preview

SlugOptions::create()
    ->generateSlugsFrom('title')
    ->saveSlugsTo('slug')
    ->startSlugSuffixFrom(5);
If my-post already exists in the database, the preview may show my-post-5 (according to Spatie rules).

Closure as slug source

SlugOptions::create()
    ->generateSlugsFrom(fn (Post $post): string => "{$post->title}-{$post->edition}")
    ->saveSlugsTo('slug');
The form must have the title and edition fields filled out — SlugField reads them from the live state of siblings in the schema.

usingSuffixGenerator — custom suffix

SlugOptions::create()
    ->generateSlugsFrom('title')
    ->saveSlugsTo('slug')
    ->usingSuffixGenerator(fn (string $slug, int $iteration): string => 'v'.($iteration + 1));

The permalink bar shows host (without https://), optional path prefix, slug segment, and optional postfix. HTTPS hosts display a green lock icon.
TitleSlugField::make(
    urlHost: 'https://wyachts.test',
    urlPath: '/blog/',
),
// Preview: wyachts.test/blog/my-post-title

Subdomain style (host only)

SlugField::make('slug')
    ->source('title')
    ->urlHost('https://acme.example.com')
    ->urlPath(null)
    ->urlHostVisible(true),

Sandwich URL (prefix + slug + postfix)

TitleSlugField::make(
    urlHost: 'https://shop.test',
    urlPath: '/products/',
    slugLabelPostfix: '/details',
),
// shop.test/products/my-product/details
TitleSlugField::make(
    visitUrl: fn (string $slug, string $routeKey, ?\Illuminate\Database\Eloquent\Model $record): string => route('blog.show', $routeKey),
    visitLinkLabel: 'View post',
),

// Lub na SlugField:
SlugField::make('slug')
    ->visitRoute(fn (string $slug, string $routeKey): ?string => filled($slug) ? route('blog.show', $routeKey) : null),
Closure visitUrl / visitRoute otrzymuje wstrzyknięte: slug (string), routeKey (string — dla self-healing modeli: hello-world-5, inaczej jak slug) i record (?Model).
SlugField::make('slug')
    ->permalinkPreview(false)
    ->showVisitLink(false)
    ->showCopyButton(false)
    ->showRegenerateButton(false),

Action button layout

Buttons sit below the input: Edit / OK / Cancel on the left; Regenerate / Copy / Visit on the right.
SlugField::make('slug')
    ->actionButtonLabels(true)   // default — text + icon
    ->actionButtonsIconOnly(),  // icons only + tooltips
Global default: config('filament-flex-fields.slug.action_button_labels').

Uniqueness validation

Separate from Spatie’s DB suffix generation — this is form validation before save.

Default (unique in table)

SlugField::make('slug'), // unique rule on slug column

Disable uniqueness check

SlugField::make('slug')->slugUnique(false),

Scoped uniqueness (tenant, locale, type, …)

SlugField::make('slug')
    ->slugUniqueModel(Post::class)
    ->slugUniqueScope(fn ($query) => $query->where('tenant_id', auth()->user()->tenant_id)),

Filament-style unique parameters

TitleSlugField::make(
    slugUniqueParameters: [
        'table' => 'posts',
        'column' => 'slug',
        'ignoreRecord' => true,
    ],
),

Homepage slug (/)

For CMS pages that should live at the site root:
SlugField::make('slug')
    ->source('title')
    ->allowHomepageSlug()
    ->slugPattern('/^(\/|[a-z0-9]+(?:-[a-z0-9]+)*)$/'),
Only the exact value / is allowed as a special case.

TitleSlugField factory parameters

TitleSlugField::make() is a static factory returning a FusedGroup (title + hidden auto-sync flag + slug).
ParameterTypeDefaultDescription
$fieldTitle?stringconfig('filament-flex-fields.slug.field_title')Title state path
$fieldSlug?stringconfig('filament-flex-fields.slug.field_slug')Slug state path
$titleField?Fieldbuilt-in FlexTextInputReplace default title control
$titleFieldWrapper?ClosurenullWrap title field: fn ($field) =&gt; $field-&gt;columnSpan(2)
$titleAfterStateUpdated?ClosurenullHook after title changes
$slugAfterStateUpdated?ClosurenullHook after slug changes / regenerate
$titleRulesarray|Closure['required', 'string']Validation on title
$slugRulesarray|Closure['required', 'string']Validation on slug
$titleAutofocusboolfalseFocus title on create
$titleReadOnlybool|ClosurefalseRead-only title
$slugReadOnlybool|ClosurefalseRead-only slug
$titleLabel?stringheadline of field nameTitle label
$titlePlaceholder?stringheadline of field nameTitle placeholder
$slugLabel?stringnull (hidden)Slug label; null hides it
$titleExtraInputAttributesarray[]Extra HTML attributes on title input
$slugUniqueParameters?arraynullPassed to slugUniqueParameters()
$titleUniqueParameters?arraynullFilament unique() na tytule
Example of titleUniqueParameters — unique title within tenant:
TitleSlugField::make(
    titleUniqueParameters: [
        'table' => 'posts',
        'column' => 'title',
        'ignoreRecord' => true,
        'where' => fn ($query) => $query->where('tenant_id', filament()->getTenant()?->id),
    ],
),
| $urlHost | ?string | config('filament-flex-fields.slug.url_host') | Permalink host | | $urlPath | ?string | null | Permalink path prefix | | $urlHostVisible | bool | true | Show host segment | | $visitLinkLabel | ?string | translated default | Visit button label | | $visitUrl | string\|Closure\|null | null | Custom visit URL; closure: slug, routeKey, record | | $showVisitLink | bool | true | Show visit action | | $slugLabelPostfix | ?string | null | Trailing URL segment after slug | | $preserveSlugOnEdit | bool\|Closure | true | Don’t auto-update slug on edit | | $translatableLocales | array\|Closure\|null | config('…slug.translatable_locales') | Enables TranslatableFields title UI; null = single-language title | | $slugSourceLocale | string\|Closure\|null | config('…slug.slug_source_locale') or app.locale | Locale whose title drives slug generation | | $requiredTitleLocales | 'all'\|list&lt;string&gt;\|Closure\|null | config('…slug.required_title_locales') or slug source locale only | Which title tabs are required (null = source locale only) | | $spatieTranslatable | bool\|Closure | false | Marks Spatie Translatable intent; hydrate auto-detects HasTranslations when package is present | | $titleLocaleConfigurator | ?Closure | null | fn (FlexTextInput $field, string $locale) =&gt; $field | | $translatableFieldsConfigurator | ?Closure | null | fn (TranslatableFields $fields) =&gt; $fields-&gt;… — full title tabs config | | $spatieModel | string\|Closure\|null | null | Spatie Sluggable only (HasSlug) — not Translatable | | $slugConfigurator | ?Closure | null | fn (SlugField $field) =&gt; $field-&gt;... | Example — custom title field + slug configurator:
use Filament\Forms\Components\RichEditor;

TitleSlugField::make(
    titleField: RichEditor::make('title')->required(),
    slugConfigurator: fn (SlugField $slug) => $slug
        ->urlHost(config('app.url'))
        ->urlPath('/news/')
        ->generationDebounce(600)
        ->maxSlugLength(120),
),
Example — custom field names via config:
// config/filament-flex-fields.php
'slug' => [
    'field_title' => 'name',
    'field_slug' => 'handle',
    'url_host' => env('APP_URL'),
],

TitleSlugField::make(), // uses name + handle

Example for each parameter of TitleSlugField::make()

TitleSlugField::make(
    // --- field names ---
    fieldTitle: 'title',
    fieldSlug: 'slug',

    // --- custom title field instead of FlexTextInput ---
    titleField: null,

    // --- title wrapper (e.g. columnSpan) ---
    titleFieldWrapper: fn ($field) => $field->columnSpanFull(),

    // --- hooks ---
    titleAfterStateUpdated: function ($state, Filament\Schemas\Components\Utilities\Set $set): void {
        // after title change (after auto-slug logic)
    },
    slugAfterStateUpdated: function ($state): void {
        // after slug change (e.g. custom validation)
    },

    // --- validation ---
    titleRules: ['required', 'string', 'min:3'],
    slugRules: ['required', 'string', 'max:255'],

    // --- title UX ---
    titleAutofocus: true,
    titleReadOnly: false,
    titleLabel: 'Post title',
    titlePlaceholder: 'Enter a descriptive title',
    titleExtraInputAttributes: ['data-test' => 'post-title'],

    // --- slug UX ---
    slugReadOnly: false,
    slugLabel: null, // null = ukryta etykieta

    // --- uniqueness ---
    slugUniqueParameters: ['column' => 'slug', 'ignoreRecord' => true],
    titleUniqueParameters: null,

    // --- permalink ---
    urlHost: config('app.url'),
    urlPath: '/blog/',
    urlHostVisible: true,
    slugLabelPostfix: null,
    visitUrl: fn (string $slug, string $routeKey, ?\Illuminate\Database\Eloquent\Model $record): string => route('blog.show', $routeKey),
    visitLinkLabel: 'View on site',
    showVisitLink: true,

    // --- behaviour ---
    preserveSlugOnEdit: true,

    // --- Spatie (does not change UI!) ---
    spatieModel: Post::class,

    // --- any SlugField configuration ---
    slugConfigurator: fn (SlugField $slug) => $slug
        ->size('lg')
        ->generationDebounce(300)
        ->showCopyButton(true),
),

SlugField — configuration API

Each method below is chainable on SlugField::make('slug').

source(string|Closure|null $statePath)

State path of the field that drives auto-generation (usually title).
SlugField::make('slug')->source('title'),
SlugField::make('data.slug')->source('data.title'), // nested

sourceLive(bool|Closure $condition = true)

When false, slug does not react to source changes (manual slug only).
SlugField::make('slug')->source('title')->sourceLive(false),

translatableTitle(bool|Closure $condition = true)

Enables translatable title source paths (title.pl, …). Usually set via titleLocales().

titleLocales(array|Closure $locales)

Locale map (['pl' =&gt; 'PL', 'en' =&gt; 'EN']) or list (['pl', 'en']). Implies translatableTitle(true).

slugSourceLocale(string|Closure $locale)

Which locale title drives slug auto-generation. Default: config slug_source_locale, then app.locale, then first locale.

translatableTitleField(string|Closure $fieldName)

Base title attribute when resolving source path (default: title).

spatieTranslatable(bool|Closure $condition = true)

Configuration flag for Spatie Translatable models. Hydration auto-detects HasTranslations on the record when spatie/laravel-translatable is installed — the flag does not need to be true for detection to work.

titleField(Field $field)

Attach a title field for SlugField::withTitle() / manual fused layouts.
SlugField::make('slug')
    ->titleField(FlexTextInput::make('title')->required()),

titleFieldWrapper(?Closure $wrapper)

SlugField::make('slug')
    ->titleField(FlexTextInput::make('title'))
    ->titleFieldWrapper(fn (Field $field) => $field->columnSpanFull()),

titleAfterStateUpdated(?Closure $callback)

->titleAfterStateUpdated(function ($state, Filament\Schemas\Components\Utilities\Set $set) {
    // runs after title change logic
}),

slugAfterStateUpdated(?Closure $callback)

->slugAfterStateUpdated(function ($state) {
    // runs when slug state changes
}),

titleReadOnly(bool|Closure $condition = true) / slugReadOnly(bool|Closure $condition = true)

Blocks editing of the respective field. Works with TitleSlugField and manual SlugField::withTitle().
// Title read-onlytu
TitleSlugField::make(titleReadOnly: true),

// Slug read-only
TitleSlugField::make(slugReadOnly: true),

// Slug read-only on edit only (recommended for published URLs)
TitleSlugField::make(
    slugConfigurator: fn (SlugField $f) => $f
        ->slugReadOnly(fn (SlugField $c): bool => $c->getOperation() === 'edit'),
),

slugifyUsing(?Closure $callback)

Custom slugifier; receives ['source' =&gt; string].
->slugifyUsing(fn (array $state): string => str_replace(' ', '.', strtolower($state['source']))),

spatieModel(string|Closure|null $modelClass)

Enable Spatie integration for preview generation.
->spatieModel(Post::class),
->spatieModel(fn () => static::getModel()),

spatieSlugField(string|Closure $attribute = 'slug')

Model attribute Spatie writes to / reads from.
->spatieSlugField('permalink'),

spatieSourceField(string|Closure|null $field)

Primary model attribute for the live source string.
->spatieSourceField('title'),

serverSideGeneration(bool|Closure $condition = true)

Use Livewire generateSlugPreview instead of client Str.slug. Automatically enabled when Spatie integration is active or when translatable titles are used.
->serverSideGeneration(), // explicit; also auto-on for Spatie + translatable titles

slugSeparator(string|Closure $separator = '-')

Normalization separator (also used by fallback SlugGenerator). Default validation slugPattern is derived from this separator automatically.
->slugSeparator('_'), // validates hello_world without manual slugPattern

maxSlugLength(int|Closure|null $length)

Max length for fallback generator; Spatie uses slugsShouldBeNoLongerThan from model.
->maxSlugLength(80),

urlHost(string|Closure|null $host) / urlPath(string|Closure|null $path)

Permalink segments. Host may include https://; display strips the scheme.
->urlHost('https://example.com')
->urlPath('/docs/'),

urlHostVisible(bool|Closure) / urlPathVisible(bool|Closure)

Controls which URL segments are visible in the permalink preview.
// Path only (no host) — e.g. in the admin panel
SlugField::make('slug')
    ->urlHost('https://example.com')
    ->urlPath('/blog/')
    ->urlHostVisible(false),

// Hide path prefix — show only host + slug
SlugField::make('slug')
    ->urlPath('/hidden-prefix/')
    ->urlPathVisible(false),

permalinkPreview(bool|Closure $condition = true)

Show or hide the entire permalink chrome.
->permalinkPreview(false),

permalinkLabel(string|Closure|null $label)

->permalinkLabel('Public URL'),

visitUrl(string|Closure|null $url) / visitRoute(string|Closure|null $route)

Target of the Visit button. The Closure receives injected parameters: slug, routeKey (self-healing: {slug}-{id}), and optionally the record.
// Named route (self-healing models need routeKey, not slug alone)
SlugField::make('slug')
    ->visitRoute(fn (string $slug, string $routeKey): ?string => filled($slug)
        ? route('posts.show', $routeKey)
        : null),

// Absolute URL
SlugField::make('slug')
    ->visitUrl(fn (string $slug, string $routeKey): ?string => filled($slug)
        ? url("/preview/{$routeKey}")
        : null),

visitLinkLabel(string|Closure|null $label)

->visitLinkLabel('Open in new tab'),
Action toggles below the slug. By default all are true (except Regenerate — visible only after manual slug edit).
SlugField::make('slug')
    ->showVisitLink(false)        // ukryj "Visit"
    ->showCopyButton(true)       // zostaw "Copy"
    ->showRegenerateButton(true), // show "Regenerate" when applicable
Example — Copy only, no Visit:
TitleSlugField::make(
    showVisitLink: false,
    slugConfigurator: fn (SlugField $s) => $s->showRegenerateButton(false),
),

actionButtonLabels(bool|Closure) / actionButtonsIconOnly(bool|Closure)

Kontrola tekstu na przyciskach akcji (Hero UI button-group + ikony Gravity).
// Text + icon (default)
SlugField::make('slug')->actionButtonLabels(true),

// Same ikony + tooltip
SlugField::make('slug')->actionButtonsIconOnly(),

// Globalnie w config
// 'slug' => ['action_button_labels' => false],

autoUpdateDisabledField(string|Closure|null $field)

Hidden boolean field path tracking manual slug edits. TitleSlugField sets {slug}_auto_update_disabled automatically.
->autoUpdateDisabledField('slug_auto_update_disabled'),

autoGenerate(bool|Closure $condition = true)

Main toggle for auto-generating slug from the source field.
// Completely manual slug (e.g. CSV import)
SlugField::make('slug')
    ->autoGenerate(false)
    ->inlineEditing(false),

preserveSlugOnEdit(bool|Closure $condition = true)

On the edit operation, it stops auto-sync from the title (protects published URL). On create, it always syncs.
// Default — do not overwrite slug on title edit
TitleSlugField::make(), // preserveSlugOnEdit: true

// Zawsze synchronizuj (jak na create)
TitleSlugField::make(preserveSlugOnEdit: false),

SlugField::make('slug')->preserveSlugOnEdit(false),

inlineEditing(bool|Closure $condition = true)

When true (default): permalink preview + Edit/OK/Cancel/Reset buttons. When false: standard TextInput.
// Prosty input bez trybu inline (np. w Repeaterze)
SlugField::make('slug')
    ->inlineEditing(false)
    ->autoGenerate(false),

allowHomepageSlug(bool|Closure $condition = true)

Allows homepage slug / (CMS homepage). Requires custom validation pattern.
SlugField::make('slug')
    ->allowHomepageSlug()
    ->slugPattern('/^(\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/')
    ->urlHost(config('app.url')),

generationDebounce(int|Closure $milliseconds = 400)

Debounce before regenerating slug from title.
->generationDebounce(250),

slugPattern(string|Closure $pattern) / regex(string|Closure|null $pattern)

Optional override. When omitted, pattern is auto-derived from slugSeparator() (and allowHomepageSlug() when enabled). regex() is an alias.
// Auto (default) — follows slugSeparator('-')
SlugField::make('slug'), // validates hello-world

// Auto with underscore separator
SlugField::make('slug')->slugSeparator('_'), // validates hello_world

// Manual override
SlugField::make('slug')->slugPattern('/^[a-z0-9]+(?:-[a-z0-9]+)*$/'),

// To samo:
SlugField::make('slug')->regex('/^[a-z0-9]+(?:-[a-z0-9]+)*$/'),

// Homepage with custom pattern (auto handles this when allowHomepageSlug only)
SlugField::make('slug')
    ->allowHomepageSlug()
    ->regex('/^(\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/'),

slugLabelPostfix(string|Closure|null $postfix)

Trailing path after slug in permalink preview.
->slugLabelPostfix('.html'),

recordSlug(string|Closure|null $slug)

Initial/stored slug for edit preview and visit link before state hydrates.
->recordSlug(fn (?Model $record): ?string => $record?->slug),

slugRules(array|Closure $rules)

Additional validation rules (besides built-in pattern and slugUnique).
SlugField::make('slug')->slugRules(['min:3', 'max:100']),

TitleSlugField::make(
    slugRules: ['required', 'string', 'regex:/^[a-z0-9-]+$/'],
),

slugUnique(bool|Closure $condition = true) / slugUniqueParameters(array $parameters) / slugUniqueScope(?Closure $scope) / slugUniqueModel(string|Closure|null $model)

Uniqueness validation in the form (independent of Spatie suffixes on save).
// Disable uniqueness check
SlugField::make('slug')->slugUnique(false),

// Scoped — tenant
SlugField::make('slug')
    ->slugUniqueModel(Post::class)
    ->slugUniqueScope(fn ($q) => $q->where('tenant_id', 1)),

// Parametry jak Filament unique()
SlugField::make('slug')->slugUniqueParameters([
    'table' => 'posts',
    'column' => 'slug',
    'ignoreRecord' => true,
]),

size(string|Closure $size) / variant(string|Closure $variant)

Size and visual variant of the field wrapper (Hero UI). Defaults from config('filament-flex-fields.ui').
SlugField::make('slug')
    ->size('lg')            // sm | md | lg
    ->variant('secondary'), // primary | secondary | …

TitleSlugField::make(
    slugConfigurator: fn (SlugField $s) => $s->size('md')->variant('primary'),
),
Defaults: config('filament-flex-fields.ui.slug_size'), slug_variant.

Inherited Filament Field API

SlugField inherits standard Filament methods — they work exactly the same as in other fields:
SlugField::make('slug')
    ->label('Adres URL')
    ->helperText('Lowercase letters, numbers, and hyphens only.')
    ->hint('Automatically generated from title')
    ->required()
    ->disabled(fn (): bool => auth()->user()?->cannot('edit-slug'))
    ->hidden(fn (): bool => ! auth()->check())
    ->columnSpanFull()
    ->columnSpan(2)
    ->live()
    ->dehydrated(true),
TitleSlugField configures title and slug separately — supply slug helper text via the slugConfigurator:
TitleSlugField::make(
    slugConfigurator: fn (SlugField $slug) => $slug
        ->helperText('This address will be visible in the URL.'),
),

Public helper methods (views, tests, extensions)

Public methods used in Blade templates, tests, and custom field extensions:
MethodReturnsWhen to use
getAlpineConfiguration()arrayDebug / custom Blade
getUiLabels()arrayUI translations in tests
getSourceStatePath()?stringLivewire path to source field
getOperation()stringcreate lub edit
generateSlugFromSource(string $source)stringServer-side slug generation logic
generateSlugPreview(string $source)stringLivewire component action endpoint
normalizeSlug(string $value)stringSlug normalization helper before database save
getFullPermalinkUrl(?string $slug)?stringFull URL for Copy/Visit actions
getDisplayUrlHost()?stringDomain host without protocol
usesSpatieIntegration()boolWhether Spatie sluggable options are resolved
getSpatieModelClass()?stringResolved FQCN model class
shouldUseServerSideGeneration()boolWhether Alpine triggers Livewire preview requests
getWrapperClasses()list&lt;string&gt;Component CSS wrapper classes
Example in test:
$field = SlugField::make('slug')->spatieModel(Post::class);

expect($field->generateSlugFromSource('Hello World'))->toBe('hello-world');
expect($field->getFullPermalinkUrl('hello-world'))
    ->toBe('https://example.com/hello-world');

FlexField schema keys (FieldType::Slug)

When using FlexFieldFormBuilder:
[
    'slug' => 'permalink',
    'label' => 'Permalink',
    'type' => 'slug',
    'config' => [
        'source' => 'title',
        'url_host' => 'https://example.com',
        'url_path' => '/posts/',
        'debounce' => 300,
        'slug_unique' => true,
        'spatie_model' => Post::class,
        'separator' => '-',
        'allow_homepage' => false,
        'preserve_on_edit' => true,
    ],
],
Config keyMaps to method
sourcesource()
url_hosturlHost()
url_pathurlPath()
debouncegenerationDebounce()
slug_uniqueslugUnique()
spatie_modelspatieModel()
separatorslugSeparator()
allow_homepageallowHomepageSlug()
preserve_on_editpreserveSlugOnEdit()

Advanced recipes

Repeater with per-row title + slug

use Filament\Forms\Components\Repeater;

Repeater::make('sections')
    ->schema([
        FlexTextInput::make('title')->required()->live(),
        SlugField::make('slug')
            ->source('title')
            ->urlHost(config('app.url'))
            ->urlPath('/sections/'),
    ])
    ->columns(1),
Nested paths are resolved automatically (sections.0.titlesections.0.slug).

Standalone slug (no title field)

SlugField::make('slug')
    ->label('URL slug')
    ->autoGenerate(false)
    ->inlineEditing(false)
    ->required(),

Regenerate button behaviour

Regenerate only appears when auto-sync has been disabled by a manual slug edit (setting the hidden field {slug}_auto_update_disabled = true). During normal syncing, the button is hidden.
// In a Livewire test — simulate manual edit:
Livewire::test(EditPost::class, ['record' => $post])
    ->set('data.slug_auto_update_disabled', true)
    ->set('data.title', 'New title'); // slug will NOT change without calling Regenerate
Note: SlugField uses wire:ignore on the Alpine fragment — changing data.slug directly in tests might not reflect UI behavior. Test by updating the title or modifying slug_auto_update_disabled.

Playground (Live Preview)

Enable the playground in .env:
FLEX_FIELDS_PLAYGROUND=true
Panel navigation: Settings & Tools → Flex Fields Playground — cluster with left sub-navigation (Filament SubNavigationPosition::Start).
  • Root URL: /admin/flex-fields-playground (redirects to first component)
  • Slug field page: /admin/flex-fields-playground/slug-field
  • Routes are registered only when FLEX_FIELDS_PLAYGROUND=true (or filament-flex-fields.playground.enabled is true).
Spatie is optional for every recipe below. All playground demos work with browser Str::slug() only. To align preview and save with laravel-sluggable, see Optional Spatie upgrade (playground recipes) at the end of this section.
Source of truth: SlugFieldPlayground in the package (src/Support/Playground/SlugFieldPlayground.php). Default form state keys (slug__title, slug__standalone, …) live in SlugFieldPlayground::defaultState().

Playground recipes — 1:1 with SlugFieldPlayground

Full section schema (copy-paste ready):
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\FlexTextInput;
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\SlugField;
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\TitleSlugField;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;

Section::make('Slug field')
    ->description('Permalink editor with inline Edit/OK/Cancel/Reset, auto-sync, unique validation hooks, Spatie Sluggable integration and FlexFields styling.')
    ->schema([
        // … recipes 1–9 below …
    ]);

Recipe 1 — Shared title source + slug field (slug__standalone)
A separate title field drives one or more slug fields on the same form. Playground reuses slug__title for recipes 1, 5, and 6.
FlexTextInput::make('slug__title') // or 'title' in your app
    ->label('Title (source)')
    ->live()
    ->columnSpanFull(),

SlugField::make('slug__standalone') // or SlugField::make('slug')
    ->label('Slug')
    ->source('slug__title') // or ->source('title')
    ->helperText('Auto-syncs from title until you edit or reset the slug.')
    ->columnSpanFull(),
DemonstratesSlugField + ->source() pointing at a sibling field (not embedded titleField())
CookbookFour ways to add title + slug — pattern 4
SpatieOptional — ->spatieModel(Post::class) on SlugField

Recipe 2 — Title + slug one-liner (TitleSlugField)
TitleSlugField::make(
    fieldTitle: 'slug__one_liner_title',
    fieldSlug: 'slug__one_liner_slug',
    urlHost: 'https://wyachts.test',
    urlPath: '/posts/',
)
    ->label('Title + slug (one-liner)')
    ->columnSpanFull(),
DemonstratesTitleSlugField fused group, permalink bar, default create/edit behaviour
CookbookScenario 1
SpatieOptional — TitleSlugField::make(..., spatieModel: Post::class)

Recipe 3 — Translatable title + single slug (slug__i18n_*)
TitleSlugField::make(
    fieldTitle: 'slug__i18n_title',
    fieldSlug: 'slug__i18n_slug',
    translatableLocales: ['pl' => 'PL', 'en' => 'EN', 'fr' => 'FR'],
    slugSourceLocale: 'pl',
    urlHost: 'https://wyachts.test',
    urlPath: '/guides/',
)
    ->label('Translatable title + slug')
    ->helperText('Single slug generated from the Polish title tab. Other locales do not change the permalink.')
    ->columnSpanFull(),
Default state shape: 'slug__i18n_title' => ['pl' => '…', 'en' => '…'], 'slug__i18n_slug' => 'przewodnik-po-morzu-srodziemnym'.
DemonstratestranslatableLocales, slugSourceLocale, TranslatableFields tabs
CookbookTranslatable titles (single slug)
SpatieOptional — spatieTranslatable: true when using Spatie Translatable

Recipe 4 — Title + slug pair (titleField() + recordSlug())
SlugField::make('slug__pair_slug')
    ->label('Title + slug pair')
    ->titleField(
        FlexTextInput::make('slug__pair_title')
            ->label('Title')
            ->placeholder('Enter a post title…'),
    )
    ->urlHost('https://wyachts.test')
    ->urlPath('/blog/')
    ->recordSlug('premium-catamaran-experience')
    ->columnSpanFull(),
DemonstratesEmbedded title via ->titleField(), ->recordSlug() for visit URL on edit
CookbookFour ways — pattern 3
SpatieOptional — ->spatieModel(Post::class)

Recipe 5 — Permalink preview + Visit link (slug__permalink)
Uses the shared title field from recipe 1 (slug__title).
SlugField::make('slug__permalink')
    ->label('Permalink preview')
    ->source('slug__title')
    ->urlHost('https://wyachts.test')
    ->urlPath('/charters/')
    ->visitRoute(fn (?string $slug): ?string => filled($slug) ? "https://wyachts.test/charters/{$slug}" : null)
    ->generationDebounce(250)
    ->columnSpanFull(),
DemonstratesurlHost, urlPath, visitRoute, generationDebounce
CookbookPermalink preview & URL actions
SpatieOptional — visit URL still works; preview uses Spatie rules when ->spatieModel() is set

Recipe 6 — URL slug sandwich (slug__sandwich)
Uses the shared title field from recipe 1 (slug__title).
SlugField::make('slug__sandwich')
    ->label('URL slug sandwich')
    ->source('slug__title')
    ->urlHost('https://wyachts.test')
    ->urlPath('/books/')
    ->slugLabelPostfix('/detail/')
    ->visitRoute(fn (?string $slug): ?string => filled($slug) ? "https://wyachts.test/books/{$slug}/detail" : null)
    ->columnSpanFull(),
Preview: wyachts.test/books/my-slug/detail/
DemonstratesslugLabelPostfix, complex visitRoute
CookbookSandwich URL
SpatieOptional

Recipe 7 — Read-only variants (grid)
Grid::make(['default' => 1, 'sm' => 2, 'lg' => 3])
    ->schema([
        SlugField::make('slug__readonly')
            ->label('Form readonly')
            ->urlHost('https://wyachts.test')
            ->urlPath('/docs/')
            ->readOnly(),

        SlugField::make('slug__slug_readonly')
            ->label('Slug readonly')
            ->urlHost('https://wyachts.test')
            ->urlPath('/docs/')
            ->slugReadOnly(),

        SlugField::make('slug__homepage')
            ->label('Homepage slug')
            ->allowHomepageSlug()
            ->urlHost('https://wyachts.test')
            ->slugPattern('/^(\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/')
            ->helperText('Supports "/" as homepage slug.'),
    ]),
DemoMethodCookbook
Form readonlyreadOnly()Scenario 10
Slug readonlyslugReadOnly()Scenario 3
Homepage /allowHomepageSlug() + slugPattern()Scenario 7
Spatie: optional for all three.

Optional Spatie upgrade (playground recipes)

None of the playground recipes require composer require spatie/laravel-sluggable. Add it only when you need model-level suffixes (-2), preventOverwrite, extraScope, or identical rules on save and in the form preview. TitleSlugField recipes (2, 3):
TitleSlugField::make(
    fieldTitle: 'title',
    fieldSlug: 'slug',
    urlHost: config('app.url'),
    urlPath: '/posts/',
    spatieModel: Post::class, // optional
),
SlugField recipes (1, 4–7):
SlugField::make('slug')
    ->source('title')
    ->spatieModel(Post::class), // optional
Full integration: Spatie laravel-sluggable integration (install, HasSlug, getSlugOptions(), #[Sluggable], translatable models).

Playground quick reference

Playground labelState key(s)Primary API
Title (source)slug__titleFlexTextInput + live()
Slugslug__standaloneSlugField + ->source('slug__title')
Title + slug (one-liner)slug__one_liner_*TitleSlugField::make(...)
Translatable title + slugslug__i18n_*translatableLocales + slugSourceLocale
Title + slug pairslug__pair_*->titleField() + ->recordSlug()
Permalink previewslug__permalinkvisitRoute + generationDebounce
URL slug sandwichslug__sandwichslugLabelPostfix + visitRoute
Form readonlyslug__readonlyreadOnly()
Slug readonlyslug__slug_readonlyslugReadOnly()
Homepage slugslug__homepageallowHomepageSlug()
// Verify playground registration in package tests
$builder = app(\Bjanczak\FilamentFlexFields\Support\FlexFieldsPlaygroundBuilder::class);
expect($builder->components())->not->toBeEmpty();

Translations

Publish translation files:
php artisan vendor:publish --tag=filament-flex-fields-translations
Override in lang/vendor/filament-flex-fields/{locale}/default.php. UI Keys (slug.*):
KeyEN (Default)PL
slug.placeholderyour-permalink-slugtwoj-adres-slug
slug.permalinkPermalinkBezposredni link
slug.badge_autoAutoAuto
slug.badge_customCustomReczny
slug.editEditEdytuj
slug.confirmOKOK
slug.cancelCancelAnuluj
slug.resetResetPrzywroc
slug.regenerateRegenerateRegeneruj
slug.copyCopyKopiuj
slug.copiedCopiedSkopiowano
slug.visitVisitOdwiedz
slug.changedChangedZmieniono
Validation (validation.slug.*):
KeyDescription
validation.slug.invalidGeneral slug validation error message
validation.slug.patternValidation error message when slug pattern mismatch
Custom labels without publishing — override directly on the field:
SlugField::make('slug')
    ->placeholder('e.g. my-article')
    ->permalinkLabel('Public link')
    ->visitLinkLabel('View post'),
Set your application locale (app.locale = pl) to load built-in translations if desired.

Troubleshooting

ProblemLikely CauseSolution
Slug does not update from titleManual edit disabled auto-syncClick Regenerate or set {slug}_auto_update_disabled to false
Slug changes on edit but shouldn’tpreserveSlugOnEdit(false)Use default TitleSlugField::make() or set preserveSlugOnEdit(true)
Preview mismatch with saved slug (no Spatie)Database duplicateThe form unique validation blocks save — change the slug
Preview suffix -2 (Spatie)Database duplicate resolved by SpatieExpected behavior — preview uses same model configuration as Spatie HasSlug
Spatie not workingMissing package dependencycomposer require spatie/laravel-sluggable
Spatie not workingModel missing getSlugOptions() or Sluggable attributeAdd slug configuration to model or pass class to spatieModel(Post::class)
usesSpatieIntegration() returns falseForm model doesn’t match class with SlugOptionsPass model class explicitly: spatieModel(Post::class)
Missing permalink barurl_host is nullSet APP_URL or -&gt;urlHost(config('app.url'))
Permalink truncates host (...)UX edit mode limits spaceNormal behavior — full URL is copied to clipboard and visited
Unique validation fails incorrectlyMissing tenant scope (multi-tenant)-&gt;slugUniqueScope(fn ($q) =&gt; $q-&gt;where('tenant_id', ...))
Unique doesn’t work on mountModel record not loaded yetPackage defers validation — ensure the resource binds the record
slugifyUsing() not workingClosure doesn’t return string or missing source stateslugifyUsing() always takes precedence over Spatie generator
Test -&gt;set('data.slug') failswire:ignore + AlpineChange data.title or set slug_auto_update_disabled flag
Target [Model] is not instantiableClosure maps to model type hint on mountUse mixed $record or remove type hint in closure parameters
Regenerate button is hiddenAuto-sync is still activeManually edit the slug first to show the button (or set update flag)
Homepage / slug rejectedDefault pattern without allowHomepageCall -&gt;allowHomepageSlug() (updates default pattern regex) or custom pattern
Repeater rows do not syncIncorrect source field path-&gt;source('title') in the same schema row resolves relative paths automatically
Diagnostics in tinker / test:
$field = SlugField::make('slug')->spatieModel(Post::class);

$field->usesSpatieIntegration();      // true/false
$field->shouldUseServerSideGeneration(); // true gdy Spatie, translatable titles lub serverSideGeneration()
$field->getSourceStatePath();         // np. "data.title"
$field->generateSlugFromSource('Hello World'); // PHP preview

Comparison with blendbyte/filament-title-with-slug

FeatureFlex Fields TitleSlugFieldblendbyte
Title + slug fused layoutYesYes
Permalink preview + HTTPS lockYesPartial
Copy URL buttonYesNo
Regenerate after manual editYesLimited
Auto / Custom badgeYesNo
Standalone SlugFieldYesNo
Scoped unique validationYesBasic
Full Spatie SlugOptions previewYesPartial
#[Sluggable] attribute supportYesNo
Multi-source generateSlugsFrom([])YesNo
Icon-only action buttons (Hero UI)YesNo
FlexField / playground integrationYesNo
Gravity iconsYesHeroicons