Browse the documentation
Building modules
Template slots
Inject or replace markup at defined points in core views from a module, using the slot registry — without editing core templates.
Template slots are predefined injection points in the application’s layouts. A module can output its own Blade content at these points without touching the core views. That is the essential point: you extend the UI without editing files that would be overwritten on the next update — keeping upgrades conflict-free.
How slots work
A slot has two halves: the core layout marks the location, and your module registers content for it.
In Blade layouts (core)
The layout contains a directive at the designated location:
@extensionSlot('admin.content.after')
This directive internally calls SlotRegistry::render('admin.content.after', $user) and outputs the collected HTML. You do not write this directive yourself — it already exists in the core layouts. Your only task is to register content for the appropriate slot name.
In your module (registration)
Registration goes through the SlotRegistry, resolved in your ServiceProvider’s boot():
$slots = app(SlotRegistry::class);
// Append content (multiple modules can append to the same slot)
$slots->append('admin.content.after', 'my-module::my-partial', ['key' => 'value']);
// Or replace a slot entirely (last-wins)
$slots->replace('admin.content.before', 'my-module::replacement');
The distinction between append and replace is intentional: append is additive and cooperative — any number of modules can contribute to the same slot. replace substitutes the entire slot content and should be used sparingly.
Available slots
The slots available depend on which layout is in use. Three layouts provide injection points: the admin layout, the driver layout, and the portal layout.
Admin layout (layouts/admin.blade.php)
| Slot name | Location | Use case |
|---|---|---|
admin.head.after | Inside <head>, after core CSS | Custom stylesheets, meta tags |
admin.sidebar.before-nav | Above navigation in sidebar | Module branding, notices |
admin.sidebar.after-nav | Below navigation in sidebar | Additional links, status info |
admin.content.before | Before main content area | Banners, warnings |
admin.content.after | After main content area | Footer widgets, debug info |
Driver layout (layouts/driver.blade.php)
| Slot name | Location | Use case |
|---|---|---|
driver.head.after | Inside <head>, after core CSS | Custom stylesheets |
driver.topbar.actions | In top bar, action area | Quick action buttons |
driver.content.before | Before main content | Notices, alerts |
driver.content.after | After main content | Additional features |
driver.bottom-nav.after | After bottom navigation | Extra nav items |
Portal layout (layouts/portal.blade.php)
| Slot name | Location | Use case |
|---|---|---|
portal.head.after | Inside <head>, after core CSS | Custom stylesheets |
portal.nav.after | After portal navigation items | Additional nav links |
portal.content.before | Before main content | Notices, seasonal info |
portal.content.after | After main content | Additional sections |
portal.footer.before | Before footer | Legal notices, links |
Slot API
Appending with append
Multiple modules can append to the same slot. Content is rendered in order, controlled by the order parameter:
$slots->append(
slotName: 'admin.content.after',
viewPath: 'my-module::partials.info-panel',
data: ['message' => 'Hello from module'],
order: 100, // sort order (lower = first)
permission: 'settings.view', // optional: only render if user has this permission
);
order determines where your content appears in the slot when other modules also contribute — a lower value moves it earlier. This keeps the ordering predictable even when you do not know which other modules are active.
Replacing with replace
replace substitutes the entire slot content. If multiple modules replace the same slot, the last one registered wins — and a warning is logged. Use replace sparingly; the warning is your signal that two modules are conflicting.
$slots->replace(
slotName: 'admin.sidebar.before-nav',
viewPath: 'my-module::sidebar-header',
data: [],
permission: null,
);
Permission gating
If a permission is specified, the slot content is only rendered for users who have that permission. This restricts content to specific roles without repeating the check inside the view itself:
$slots->append('admin.content.after', 'my-module::admin-only', [], 100, 'settings.edit');
// Only visible to users with 'settings.edit' permission
Which permissions exist and how a module registers its own is covered in Permissions & roles.
Example: adding a banner
In your ServiceProvider’s boot(), register the partial …
// In your ServiceProvider boot()
$slots = app(SlotRegistry::class);
$slots->append('admin.content.before', 'my-module::banners.update-available', [
'version' => '2.0.0',
], 50);
… and the corresponding Blade view renders the actual content:
{{-- resources/views/banners/update-available.blade.php --}}
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<p class="text-blue-800">
Module-Update {{ $version }} verfügbar.
</p>
</div>
Values passed via data are available in the view as variables — here $version.
Example: adding a tab to the portal
Often two slots work together: one for the navigation link, one for the content it reveals. To add a dedicated section to the portal:
// Append navigation link
$slots->append('portal.nav.after', 'my-module::nav.documents-link');
// Append content section
$slots->append('portal.content.after', 'my-module::portal.documents-section');
Slots are just one of several extension points a ServiceProvider registers. How the SlotRegistry fits alongside the other registries in a module’s boot sequence is shown in The ServiceProvider pattern; the full registry API is in Registries.