Livewire 4 Slots: Pass Content Into Components Like Blade

Livewire 4 slots let you pass default and named content into a component while wire:click stays bound to the parent. Build reusable modals, cards, and panels.

Steven Richardson
Steven Richardson
· 7 min read

Before Livewire 4, building a reusable modal or card shell meant passing HTML through properties, nesting Blade components inside, or copy-pasting markup across every page that needed it. Livewire 4 slots fix that. You pass markup into a component exactly like you would with a Blade component, and that markup stays wired to the parent. This guide walks through default slots, named slots, and the one reactivity rule that catches everyone — building a reusable confirm-modal as we go.

Slots are one of the headline additions in v4. If you're coming from v3, it's worth skimming the full set of changes in the Livewire 3 to 4 migration guide first, because the component file format changed too.

Generate a slotted Livewire component#

Start by generating a single-file component that will act as the shell. In Livewire 4 the default make:livewire output is a view-first single-file component that lives alongside your Blade components and carries a ⚡ prefix in its filename.

php artisan make:livewire modal

That creates resources/views/components/⚡modal.blade.php. Replace its contents with a minimal shell that can open and close itself, leaving a {{ $slot }} placeholder where the parent's content will land:

<?php // resources/views/components/⚡modal.blade.php

use Livewire\Component;

new class extends Component {
    public bool $open = false;

    public function toggle(): void
    {
        $this->open = ! $this->open;
    }
};
?>

<div>
    <button type="button" wire:click="toggle">Open</button>

    <div wire:show="open" class="modal">
        {{ $slot }}
    </div>
</div>

The wire:click="toggle" here lives in the child's own template, so it calls the child's toggle() method. Hold that thought — it matters once we put wire:click inside slot content.

Render parent content with the default slot#

The default slot is whatever you place between the component's opening and closing tags. Render the component with <livewire:modal> and drop any markup inside it; the child outputs that markup wherever it prints {{ $slot }}.

<livewire:modal>
    <p>Your changes have been saved.</p>
</livewire:modal>

That <p> is injected in place of {{ $slot }} in the modal. This is identical to how Blade component slots behave, which is the whole point — there's nothing new to learn for the common case. Anything you can write in a Blade template, you can pass through the default slot.

A modal usually needs more than one injection point: a title at the top, actions at the bottom, body content in the middle. Named slots cover that. In the parent, wrap each region in <livewire:slot name="...">; everything left over becomes the default slot.

<livewire:modal>
    <livewire:slot name="header">
        <h2>Delete post</h2>
    </livewire:slot>

    <p>This will permanently remove the post. This action cannot be undone.</p>

    <livewire:slot name="footer">
        <button type="button">Cancel</button>
    </livewire:slot>
</livewire:modal>

In the child, named slots are read from the $slots variable using array access — $slots['header'] — while the leftover content stays in $slot:

<div>
    <button type="button" wire:click="toggle">Open</button>

    <div wire:show="open" class="modal">
        <header>{{ $slots['header'] }}</header>

        <div class="modal-body">
            {{ $slot }}
        </div>

        <footer>{{ $slots['footer'] }}</footer>
    </div>
</div>

Note the distinction: the default slot is the singular $slot, named slots come from the plural $slots collection. Mixing those two up is the most common first-time mistake.

Call parent methods from slot content#

Here's the rule that trips people up: slot content is evaluated in the parent's context, not the child's. Any wire:click, wire:model, or property reference inside a slot targets the parent component, because that's where the markup actually belongs. The child is just deciding where to print it.

<livewire:modal>
    <livewire:slot name="header">
        <h2>Delete "{{ $post->title }}"</h2>
    </livewire:slot>

    <p>This will permanently remove the post. This action cannot be undone.</p>

    <livewire:slot name="footer">
        {{-- deletePost() lives on the PARENT, not the modal --}}
        <button type="button" wire:click="deletePost({{ $post->id }})">
            Delete
        </button>
    </livewire:slot>
</livewire:modal>

For that button to work, deletePost() and $post live on the parent component that renders the modal — for example a ⚡post-card.blade.php:

<?php // resources/views/components/⚡post-card.blade.php

use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    public function deletePost(int $id): void
    {
        Post::findOrFail($id)->delete();
    }
};
?>

<div>
    {{-- the <livewire:modal> usage above goes here --}}
</div>

The modal never needs to know what "delete" means. It owns its own open/close state via toggle(), while the parent owns the action. This is exactly the separation you want for a shell component, and it pairs well with driving modal open/close state through Alpine and wire:entangle when you want the toggle to feel instant on the client.

Conditionally render optional Livewire 4 slots#

Not every caller supplies every slot. A reusable modal might have an optional footer. Use $slots->has('name') to check whether the parent passed a given named slot, and skip the surrounding wrapper markup when it didn't.

<div>
    <button type="button" wire:click="toggle">Open</button>

    <div wire:show="open" class="modal">
        @if ($slots->has('header'))
            <header>{{ $slots['header'] }}</header>
        @endif

        <div class="modal-body">
            {{ $slot }}
        </div>

        @if ($slots->has('footer'))
            <footer>{{ $slots['footer'] }}</footer>
        @endif
    </div>
</div>

Without the has() guard you'd render an empty <header> or <footer> element even when no content was passed, which leaves stray borders and padding in your layout. Checking first keeps the rendered DOM clean.

Choose between Livewire 4 slots, props, and islands#

Slots are for passing markup the parent owns. They are not the right tool when the child needs the actual value to do something with it. If the child has to loop over a collection, validate it, or react when it changes, pass a prop instead — and if the child must update when the parent's value changes, make it a reactive prop with the #[Reactive] attribute.

Likewise, if your goal is to isolate re-rendering rather than inject content, reach for an island instead of extracting a child component. The Livewire team is explicit that islands give you isolated updates without the prop-and-event overhead of a nested component. A good rule of thumb: slots for layout and composition, props for data the child consumes, islands for performance isolation.

Wrapping Up#

Default slots cover the common case with zero ceremony, named slots via <livewire:slot name="..."> handle multi-region shells, and $slots->has() keeps optional regions tidy. The single rule to remember is that slot content runs in the parent's context, so wire:click inside a slot calls the parent.

From here, the natural next step is to put slots to work on something real — a multi-step wizard where each step lives in its own slot, or study how Flux Pro's server-driven table component leans on this same slot-and-attribute composition under the hood.

FAQ#

How do slots work in Livewire 4?

You pass Blade markup between a component's opening and closing tags, and the component decides where to print it using the $slot variable. Livewire 4 supports both a default slot and multiple named slots, mirroring Blade component slots. The injected content is rendered in the parent's context, so it can reference the parent's properties and methods directly.

What is the difference between a default slot and a named slot in Livewire?

The default slot is the unnamed content placed directly inside the component tag, and the child renders it with {{ $slot }}. A named slot is wrapped in <livewire:slot name="header"> in the parent and read in the child with array access on the $slots collection, like {{ $slots['header'] }}. Use the default slot for the main body and named slots for distinct regions such as a header or footer.

Does a wire:click inside a Livewire slot call the parent or the child component?

It calls the parent. Slot content is evaluated in the context of the parent component that supplied it, so any wire:click, wire:model, or property reference inside the slot targets the parent, not the child component rendering the slot. That is why a "Delete" button passed into a modal can call a deletePost() method that lives on the parent.

When should I use a Livewire slot instead of a prop?

Use a slot when you want the parent to inject markup into a reusable shell, such as a modal, card, or panel. Use a prop when the child component needs the actual value to work with — to loop over it, validate it, or render it itself — and use a #[Reactive] prop when the child must update as the parent's value changes. Slots are about composition; props are about data.

Can I conditionally render a named slot in Livewire?

Yes. Call $slots->has('name') in the child template to check whether the parent supplied that named slot, then render the surrounding markup only when it returns true. This prevents empty wrapper elements — like an empty footer with leftover borders — from appearing when a caller omits the optional slot.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.