Key takeaways
- ✓ Select / CheckboxList: pick existing related records. Repeater with
relationship(): edit a few children inline in the parent form. - ✓ Relation manager: a table of children under the parent’s edit or view page, managed in modals.
- ✓ Relation page (
ManageRelatedRecords): the same table on its own page, in the resource’s sub-navigation. - ✓ Nested resource: children too complex for modals get full create and edit pages, with URLs like
/courses/1/lessons/5/edit.
Also Read: Filament v3 to v5 Upgrade: PHP Skip v4 Without Breaking Your Panel
“How do I manage the lessons of a course?” has at least five correct answers in Filament, and choosing the wrong one creates UX debt that’s hard to undo. This guide explains each option with code, and gives you a decision table so the choice becomes obvious.
The decision table
| Your children are… | Use | UX |
|---|---|---|
| Existing records you link (tags, categories) | Select::relationship()->multiple() or CheckboxList | Field in the parent form |
| Few, simple, saved with the parent (invoice lines, FAQs) | Repeater::relationship() | Inline rows in the parent form |
| Many, with their own lifecycle, simple forms (comments, payments) | Relation manager | Table under the parent, modals |
| As above, but deserving their own page and nav item | Relation page | Separate page, same table |
| Complex (tabs, wizards, their own relation managers) | Nested resource | Full create and edit pages |
| Important on their own, across parents (all orders) | A normal top-level resource, often as well | Global list |
Here’s how each one works.
Option 1: Select and CheckboxList for linking existing records
use Filament\Forms\Components\Select;
Select::make('tags')
->relationship('tags', 'name')
->multiple()
->preload()
->searchable()
->createOptionForm([
TextInput::make('name')->required(),
]),createOptionForm() lets users add a missing tag without leaving the form. This is the right tool when the relationship is the data, for example a post’s categories.
Option 2: Repeater for inline children
use Filament\Forms\Components\Repeater;
Repeater::make('items')
->relationship()
->schema([
Select::make('product_id')->relationship('product', 'name')->required(),
TextInput::make('quantity')->numeric()->default(1)->required(),
TextInput::make('unit_price')->numeric()->prefix('£')->required(),
])
->columns(3)
->orderColumn('sort')
->defaultItems(1),Also Read: Laravel and PHP
Children are saved with the parent in one form submission, which suits invoice lines, product variants and FAQ entries. Since v4, repeaters can also render as a compact table (->table([...])), so the old table-repeater plugins are no longer needed. Avoid repeaters for dozens or hundreds of children, because every row renders in the form.
Option 3: Relation managers
A relation manager is a Livewire table of related records shown beneath the parent’s edit or view page, with modal create and edit.
php artisan make:filament-relation-manager CourseResource lessons titlenamespace App\Filament\Resources\Courses\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class LessonsRelationManager extends RelationManager
{
protected static string $relationship = 'lessons';
public function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('title')->required()->maxLength(255),
TextInput::make('duration_minutes')->numeric(),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->reorderable('sort')
->columns([
TextColumn::make('title')->searchable(),
TextColumn::make('duration_minutes')->suffix(' min'),
])
->headerActions([CreateAction::make()])
->recordActions([EditAction::make(), DeleteAction::make()]);
}
public static function getBadge(Model $ownerRecord, string $pageClass): ?string
{
return (string) $ownerRecord->lessons()->count();
}
}Register it on the parent resource:
public static function getRelations(): array
{
return [
LessonsRelationManager::class,
];
}Also Read: Laravel: Livewire 4 Islands
Things worth knowing:
- Read-only on view pages. By default, relation managers on a view page hide create, edit and delete actions. Override
isReadOnly(): boolto returnfalseif you want editing there. - Conditional display.
canViewForRecord(Model $ownerRecord, string $pageClass): boolhides the manager for some parents, for example only published courses. - Grouping.
RelationGroup::make('Content', [LessonsRelationManager::class, QuizzesRelationManager::class])puts several managers under one tab. - Performance. Each relation manager is its own Livewire component, so a page with six managers isn’t one giant render. Still, only register the ones people use.
HasMany vs BelongsToMany: associate and attach
For HasMany, you can re-parent existing records with AssociateAction and DissociateAction. For BelongsToMany, you link existing records with AttachAction and DetachAction, including pivot data:
use Filament\Actions\AttachAction;
use Filament\Actions\DetachAction;
->headerActions([
AttachAction::make()
->preloadRecordSelect()
->schema(fn (AttachAction $action): array => [
$action->getRecordSelect(),
Select::make('role')
->options(['owner' => 'Owner', 'member' => 'Member'])
->required(),
]),
])
->recordActions([
EditAction::make(),
DetachAction::make(),
])
->columns([
TextColumn::make('name'),
TextColumn::make('role'), // pivot column
])Define the pivot columns on the relationship (->withPivot('role')) and include them in the relation manager’s form, so EditAction can change them.
Tabs next to the form, titles and lazy loading
A few relation manager settings that make a noticeable UX difference:
// On the EditCourse page: show the form and relation managers as tabs
public function hasCombinedRelationManagerTabsWithContent(): bool
{
return true;
}Also Read: Filament on Laravel 13: The Complete Compatibility Checklist
With this on, “Details”, “Lessons” and “Students” become tabs at the top of the page, instead of the relation managers stacking under a long form. Users stop scrolling past fields to reach related records.
// On the relation manager: dynamic titles
public static function getTitle(Model $ownerRecord, string $pageClass): string
{
return "Lessons in {$ownerRecord->title}";
}Relation managers are lazy-loaded by default, so the parent page renders first and each manager’s table loads afterwards. Leave that on for heavy managers. If a manager is small and always visible, disabling lazy loading (protected static bool $isLazy = false;) avoids the short loading flash.
Relation managers and the parent’s policies
Relation manager actions (create, edit, delete, attach, detach) are authorised with the related model’s policy. A LessonPolicy that denies create() hides the manager’s Create button, even for someone who can edit the course. Whether the manager appears at all is controlled by canViewForRecord(). If buttons disappear unexpectedly, check the related model’s policy first. Our guide to roles and permissions in Filament explains how Filament maps actions to policy methods.
Option 4: Relation pages
A relation page shows the same table as a relation manager, but as a separate page in the resource’s sub-navigation instead of under the edit form. That’s useful when the parent form is long, or when you want a dedicated URL:
php artisan make:filament-page ManageCourseLessons --resource=CourseResource --type=ManageRelatedRecordsAlso Read: Build a Multi-Tenant SaaS Panel PHP with Filament v5 (Step by Step)
Register it in getPages() and add it to the record sub-navigation:
public static function getPages(): array
{
return [
'index' => ListCourses::route('/'),
'edit' => EditCourse::route('/{record}/edit'),
'lessons' => ManageCourseLessons::route('/{record}/lessons'),
];
}
public static function getRecordSubNavigation(Page $page): array
{
return $page->generateNavigationItems([
EditCourse::class,
ManageCourseLessons::class,
]);
}Option 5: Nested resources
When a lesson has a rich editor, video uploads, a quiz builder and its own relation managers, a modal isn’t enough. Filament v4 introduced nested resources: full resource pages scoped to a parent record. From the nested resources docs:
php artisan make:filament-resource Lesson --nested
php artisan make:filament-relation-manager CourseResource lessons titleAlso Read: Claude Opus 5.5 Released: Price Cut, Benchmarks & Breaking Changes for Developers
When the relation manager generator asks whether rows should link to a resource instead of opening modals, answer yes and choose LessonResource. You’ll get:
// On the relation manager (or relation page)
protected static ?string $relatedResource = LessonResource::class;
// On the nested LessonResource
protected static ?string $parentResource = CourseResource::class;URLs become /admin/courses/1/lessons/5/edit, breadcrumbs show Courses › Intro to Laravel › Lessons › Routing, and the nested resource can have its own relation managers, such as a lesson’s attachments.
If your relationships don’t follow naming conventions, replace $parentResource with an explicit registration:
use Filament\Resources\ParentResourceRegistration;
public static function getParentResourceRegistration(): ?ParentResourceRegistration
{
return CourseResource::asParent()
->relationship('lessons')
->inverseRelationship('course');
}When the parent has several relation managers, register them with string keys so redirects back from the nested resource land on the right tab:
public static function getRelations(): array
{
return [
'lessons' => LessonsRelationManager::class,
'students' => StudentsRelationManager::class,
];
}Tenancy and relationships
In a multi-tenant panel, relation managers are scoped through their parent. Still check Select::relationship() option lists and AttachAction record selects. They must only offer the current tenant’s records, and scopedExists() guards validation.
Testing relation managers
use function Pest\Livewire\livewire;
it('lists a course\'s lessons', function () {
$course = Course::factory()->has(Lesson::factory()->count(3))->create();
livewire(LessonsRelationManager::class, [
'ownerRecord' => $course,
'pageClass' => EditCourse::class,
])
->assertOk()
->assertCanSeeTableRecords($course->lessons);
});Pass ownerRecord and pageClass, and everything else works like testing a table. More in our Filament Pest testing guide.
FAQ
What is a relation manager in Filament?
A relation manager is a table of related records (such as a post’s comments) shown beneath a resource’s edit or view page, with actions to create, edit, attach or delete related records, usually in modals.
When should I use a nested resource instead of a relation manager?
Use a nested resource when related records need full-page create and edit screens: complex forms, their own relation managers, or deep links. Use a relation manager when modal forms are enough.
Why can’t I edit records in a relation manager on the view page?
Relation managers are read-only on view pages by default. Override isReadOnly() to return false.
How do I save pivot data with AttachAction?
Add pivot fields next to $action->getRecordSelect() in AttachAction::make()->schema(...), and declare the pivot columns with ->withPivot() on the relationship.
Sources and further reading
- Filament: managing relationships
- Filament: nested resources
- Filament repeater
- Filament testing resources and relation managers
- Laravel Eloquent relationships
Related on The Web Tier: Soft deletes and trash views · Audit logging in Filament
