Skip to content

Blade Integration

Hyper provides several Blade directives that make working with reactive interfaces feel natural within Laravel's templating system. These directives bridge the gap between PHP and Datastar's reactive frontend, letting you write clean, maintainable code.

Core Directives

@hyper

The @hyper directive includes Hyper's JavaScript and sets up CSRF token handling. Place it in your layout's <head> section:

blade
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Application</title>

    @hyper
</head>
<body>
    @yield('content')
</body>
</html>

What it generates:

html
<meta name="csrf-token" content="your-csrf-token-here">
<script type="module" src="/vendor/hyper/js/hyper.js"></script>

The CSRF token meta tag is automatically read by Hyper's CSRF-enabled actions (@postx, @putx, @patchx, @deletex), providing transparent protection against CSRF attacks without manual token management.

The script tag uses type="module", loading Hyper as an ES module. This is supported by all modern browsers and loads asynchronously without blocking page rendering.

One-Time Setup

Add @hyper to your main layout once. All views extending that layout automatically have access to Hyper's functionality.

@signals

The @signals directive (Hyper extension) initializes reactive signals from PHP data, providing a clean bridge between your Laravel backend and Datastar's reactive frontend.

Basic Usage

blade
<div @signals(['count' => 0, 'message' => 'Hello'])>
    <p data-text="$count"></p>
    <p data-text="$message"></p>
</div>

This creates two signals: count initialized to 0 and message initialized to 'Hello'. These signals are immediately available throughout the page via Datastar's $ prefix.

Variable Syntax

The directive supports special variable naming conventions for creating different signal types:

blade
@php
    $username = 'John Doe';
    $editing = false;
    $userId = 123;
@endphp

<div @signals($username, $_editing, $userId_)>
    <!-- Creates three signals:
         - username: 'John Doe' (regular signal)
         - _editing: false (local signal, stays in browser)
         - userId_: 123 (locked signal, protected from tampering)
    -->
</div>

Variable naming rules:

  • $variable → Creates regular signal variable (sent to server)
  • $_variable → Creates local signal _variable (stays in browser, never sent to server)
  • $variable_ → Creates locked signal variable_ (encrypted in session, validated for tampering)

Local signals (_) are useful for UI state that doesn't need server processing (dropdown visibility, accordion states). Locked signals (_ suffix) provide security for sensitive data like user IDs, roles, or pricing information.

Spread Syntax

Spread arrays into signals using the ... operator:

blade
@php
    $contact = [
        'name' => 'Jane Smith',
        'email' => 'jane@example.com',
        'phone' => '555-0123'
    ];
@endphp

<form @signals(...$contact)>
    <!-- Creates signals: name, email, phone -->
    <input data-bind="name" />
    <input data-bind="email" />
    <input data-bind="phone" />
</form>

Spread with type prefixes:

blade
@php
    $uiState = ['showModal' => false, 'activeTab' => 'profile'];
    $secureData = ['userId' => 42, 'role' => 'admin'];
@endphp

<div @signals(...$_uiState, ...$secureData_)>
    <!-- Local signals: _showModal, _activeTab (stay in browser)
         Locked signals: userId_, role_ (protected from tampering) -->
</div>

The prefix applies to all keys in the spread array.

Mixing Syntaxes

Combine arrays, variables, and spreads in a single directive:

blade
<div @signals(['count' => 0], $username, ...$contact, $_editing)>
    <!-- Creates signals from all sources -->
</div>

Automatic Type Conversion

The directive automatically converts PHP objects to JavaScript-compatible formats:

Eloquent Models:

blade
<form @signals(...$user)>
    <!-- All model attributes become signals -->
    <input data-bind="name" />
    <input data-bind="email" />
</form>

Collections:

blade
<div @signals(['tags' => $tagCollection])>
    <!-- Collection converted to JavaScript array -->
    <template data-for="tag in $tags">
        <span data-text="tag.name"></span>
    </template>
</div>

Paginators:

blade
<div @signals(['users' => $usersPaginated])>
    <!-- Paginator converted to array of items -->
</div>

Carbon Dates:

blade
<div @signals(['createdAt' => $post->created_at])>
    <!-- Carbon date converted to ISO 8601 string -->
    <time data-text="$createdAt"></time>
</div>

The directive handles any object implementing Arrayable or JsonSerializable, making it seamless to pass Laravel's data structures to the frontend.

Locked Signals Security

Locked signals can only be created using the @signals directive, not data-signals. The directive hooks into PHP-side processing to encrypt and validate locked signals. Using data-signals with the _ suffix won't provide security protection.

Learn more in Advanced: Locked Signals.

@fragment / @endfragment

Fragments define reusable sections within your Blade views that can be rendered and updated independently. The key insight is that fragments are about code reuse—you can render a fragment to update any part of your page, with or without explicit targeting.

Defining Fragments

Wrap any section of your Blade view with @fragment and @endfragment:

blade
<!-- resources/views/todos.blade.php -->
<x-layouts.app>
    <div class="container">
        <h1>Manage Your Todos</h1>

        @fragment('todo-list')
        <div id="todo-list">
            @forelse ($todos as $todo)
                <x-todos.item :todo="$todo" />
            @empty
                <p class="empty-state">No todos found</p>
            @endforelse
        </div>
        @endfragment
    </div>
</x-layouts.app>

Fragment names must be unique within a view. If multiple fragments share the same name, an exception is thrown.

Rendering Fragments from Controllers

Basic Usage (Automatic Targeting):

Use hyper()->fragment() to render just the fragment. Datastar automatically targets elements by ID using its default outer mode (morphing):

php
public function store()
{
    $validated = signals()->validate(['title' => 'required|string|max:255']);
    Todo::create($validated);

    $todos = Todo::all();

    return hyper()->fragment('todos', 'todo-list', compact('todos'));
}

This renders only the content between @fragment('todo-list') and @endfragment, avoiding the overhead of compiling the full view. Datastar then:

  1. Looks for an element with id="todo-list" (matching the root element's ID in the fragment)
  2. Uses outer mode (default) to morph the element, preserving state like event listeners and focus
  3. Efficiently updates only what changed in the DOM

ID-Based Targeting (Datastar)

Datastar's default behavior matches fragments to elements by ID. The fragment's root element ID determines where it patches. This works without explicit options most of the time.

Example pattern:

blade
<!-- resources/views/contacts/index.blade.php -->
@fragment('page')
<div id="page" class="container">
    <h1>Manage Your Contacts</h1>

    @fragment('list-with-pages')
    <div id="list-with-pages">
        @foreach($contacts as $contact)
            <!-- Contact cards -->
        @endforeach

        <div class="pagination">
            {{ $contacts->links() }}
        </div>
    </div>
    @endfragment
</div>
@endfragment
php
// Controller
public function index()
{
    $contacts = Contact::query()
        ->when(request('search'), fn($q) => $q->search(request('search')))
        ->paginate(10);

    return request()->isHyperNavigate(['pagination', 'filters'])
        ? hyper()->fragment('contacts.index', 'list-with-pages', compact('contacts'))
        : hyper()->fragment('contacts.index', 'page', compact('contacts'))
              ->web(view('contacts.index', compact('contacts')));
}

This approach updates just the list when filtering/paginating, or the entire page for navigation.

Explicit Targeting with Options

When you need precise control, pass options to override Datastar's defaults:

php
return hyper()->fragment('view-name', 'fragment-name', $data, [
    'selector' => '#target-element',  // Override ID-based targeting
    'mode' => 'inner'                 // Override default 'outer' mode
]);

When to use options:

  • Different target element: Fragment content should patch somewhere other than its root ID
  • Different patch mode: Need inner, append, prepend, etc. instead of default outer
  • Dynamic IDs: When working with model-based IDs that change per item

Example with explicit options:

blade
<!-- resources/views/components/todos/item.blade.php -->
<div id="todo-{{ $todo->id }}">
    <div id="todo-label-{{ $todo->id }}">
        @fragment('todo-label')
            @if($editing)
                <input data-bind="title" value="{{ $todo->title }}" />
                <button data-on:click="@postx('{{ route('todos.update', $todo) }}')">
                    Save
                </button>
            @else
                <span>{{ $todo->title }}</span>
                <button data-on:click="@get('{{ route('todos.edit', $todo) }}')">
                    Edit
                </button>
            @endif
        @endfragment
    </div>
</div>
php
public function edit(Todo $todo)
{
    return hyper()->fragment('todos.item', 'todo-label', [
        'todo' => $todo,
        'editing' => true,
    ], [
        'selector' => '#todo-label-' . $todo->id,
        'mode' => 'inner'  // Preserve container, update content only
    ]);
}

Here we explicitly target #todo-label-{id} and use inner mode to preserve the wrapping div while updating only its contents.

Understanding Datastar Patch Modes

Mode - Determines how to update the element (Datastar):

  • 'outer' (default) - Morphs the outer HTML, intelligently preserving element state (event listeners, focus, CSS transitions)
  • 'inner' - Morphs the inner HTML, preserving the container element itself
  • 'replace' - Completely replaces the outer HTML (no morphing, loses state)
  • 'append' - Adds content as the last child
  • 'prepend' - Adds content as the first child
  • 'before' - Inserts content as a sibling before the target
  • 'after' - Inserts content as a sibling after the target
  • 'remove' - Removes the target element

Morphing vs Replace

outer mode (default) uses morphing to intelligently update elements while preserving JavaScript state. replace mode completely replaces elements, potentially losing event listeners and focus. Always prefer morphing modes (outer, inner) unless you specifically need a clean replacement.

Nested Fragments

Fragments can be nested for granular updates:

blade
<!-- resources/views/contacts/index.blade.php -->
@fragment('page')
<div id="page">
    <h1>Manage Your Contacts</h1>

    @fragment('list-with-pages')
    <div id="list-with-pages">
        @foreach($contacts as $contact)
            <!-- Contact cards -->
        @endforeach

        <div class="pagination">
            {{ $contacts->links() }}
        </div>
    </div>
    @endfragment
</div>
@endfragment

Render the outer fragment to update everything, or render just the inner fragment to update the list:

php
// Full page update (navigation)
return hyper()->fragment('contacts.index', 'page', compact('contacts'));

// Partial update (filtering/pagination)
return hyper()->fragment('contacts.index', 'list-with-pages', compact('contacts'));

Both fragments rely on ID-based targeting—no explicit options needed in most cases

Multiple Fragments

Update multiple fragments in a single response using hyper()->fragments():

php
return hyper()->fragments([
    [
        'view' => 'dashboard',
        'fragment' => 'user-stats',
        'data' => ['postCount' => 42, 'followerCount' => 128],
    ],
    [
        'view' => 'dashboard',
        'fragment' => 'recent-activity',
        'data' => ['activities' => $activities],
    ],
]);

This sends multiple SSE events, each patching a different fragment. The UI updates atomically—all fragments update together.

When to Use Fragments

Use fragments when:

  • Updating specific sections without re-rendering the entire view
  • Different user actions need different levels of page updates (full page vs partial list)
  • Building inline editing interfaces where sections toggle between view and edit states
  • Organizing complex views into logical, reusable sections

Common patterns:

  • Pagination/Filtering: Update just the list fragment, not the entire page
  • Inline Editing: Render the same fragment with different $editing state
  • Nested Updates: Full page fragment for navigation, partial fragment for interactions
  • Conditional Rendering: Show different fragments based on request context (isHyperNavigate)

Fragment Organization

Fragments work best as self-contained sections with their own data requirements. Most fragments don't need explicit targeting—Datastar's ID-based matching handles it automatically.

@ifhyper / @endifhyper

The @ifhyper directive (Hyper extension) conditionally renders content only when the request is a Hyper request, allowing you to serve different content for reactive updates versus full page loads.

Basic Usage

blade
<div>
    <h1>Contact Form</h1>

    @ifhyper
        <a href="{{ route('contacts') }}" data-navigate="true" class="back-link">
            ← Back to List
        </a>
    @endifhyper

    <form>
        <!-- Form fields -->
    </form>
</div>

On initial page load, the "Back to List" link is not rendered. When the same view is fetched via a Hyper request (e.g., through data-navigate or an action like @postx), the link appears.

How It Works

The directive checks for the Datastar-Request header:

php
// HyperServiceProvider.php (line 139-141)
Blade::if('ifhyper', function () {
    return request()->hasHeader('Datastar-Request');
});

Datastar automatically includes this header with every HTTP action request, allowing you to distinguish between full page loads and reactive requests.

Use Cases

Progressive Enhancement:

Show enhanced navigation for reactive requests while maintaining standard navigation for full page loads:

blade
@ifhyper
    <nav>
        <a href="{{ route('home') }}" data-navigate="true">Home</a>
        <a href="{{ route('about') }}" data-navigate="true">About</a>
    </nav>
@else
    <nav>
        <a href="{{ route('home') }}">Home</a>
        <a href="{{ route('about') }}">About</a>
    </nav>
@endifhyper

Conditional Fragments:

Include fragment markup only when rendering for Hyper requests:

blade
@ifhyper
    @fragment('dynamic-content')
    <div id="content">
        {{ $content }}
    </div>
    @endfragment
@else
    <div id="content">
        {{ $content }}
    </div>
@endifhyper

Loading Indicators:

Show reactive loading states only for Hyper requests:

blade
@ifhyper
    <div data-show="$loading">Loading...</div>
@endifhyper

Different Layouts:

Serve minimal layouts for reactive requests while full layouts for page loads:

blade
@ifhyper
    <!-- Minimal reactive layout without header/footer -->
    @yield('content')
@else
    <x-layouts.app>
        @yield('content')
    </x-layouts.app>
@endifhyper

Programmatic Check

You can also check programmatically in controllers using request()->isHyper():

php
public function show()
{
    if (request()->isHyper()) {
        // Return minimal view for reactive requests
        return hyper()->view('contacts.show-minimal', $data);
    }

    // Return full view for page loads
    return view('contacts.show', $data);
}

Both @ifhyper and request()->isHyper() check the same underlying header, ensuring consistency across your application.

Graceful Degradation

Use @ifhyper to enhance the experience for reactive requests while maintaining full functionality for traditional navigation. This ensures your application works with or without JavaScript.

Datastar Actions in Blade

While not strictly Blade directives, Datastar's action syntax is used extensively within Blade templates. Hyper extends these actions with Laravel-specific functionality.

CSRF-Protected Actions (Hyper Extensions)

Hyper provides CSRF-protected versions of Datastar's HTTP actions:

blade
<!-- Regular Datastar actions (no CSRF protection) -->
<button data-on:click="@post('/endpoint')">Submit</button>
<button data-on:click="@get('/data')">Fetch</button>
<button data-on:click="@put('/endpoint')">Update</button>
<button data-on:click="@patch('/endpoint')">Patch</button>
<button data-on:click="@delete('/endpoint')">Delete</button>

<!-- Hyper CSRF-protected actions -->
<button data-on:click="@postx('/endpoint')">Submit</button>
<button data-on:click="@putx('/endpoint')">Update</button>
<button data-on:click="@patchx('/endpoint')">Patch</button>
<button data-on:click="@deletex('/endpoint')">Delete</button>

The x suffix versions automatically include Laravel's CSRF token from the <meta name="csrf-token"> tag created by @hyper. This provides transparent CSRF protection without manual token management.

When to use each:

  • Use @postx, @putx, @patchx, @deletex for any mutating operations
  • Use @get, @post (without x) only when CSRF protection is explicitly not needed (e.g., public endpoints)

Default to CSRF Protection

Always use the x versions (@postx, etc.) unless you have a specific reason not to. CSRF protection should be the default, not the exception.

@fileUrl Action (Hyper Extension)

The @fileUrl action converts file signals into valid HTML src or href URLs, handling both base64-encoded files from data-bind inputs and server-side file paths.

blade
<input type="file" data-bind="profilePicture" accept="image/*" />

<img data-attr:src="@fileUrl($profilePicture)" alt="Profile Picture" />

What it does:

  1. If $profilePicture contains a base64-encoded file (from Datastar's file input binding), it converts the array to a data URL: data:image/png;base64,...
  2. If $profilePicture is a file path (sent from server), it returns the path as-is: /storage/profile.jpg
  3. If $profilePicture is empty or null, it returns an empty string (or your custom fallback)

With fallback:

blade
<img data-attr:src="@fileUrl($profilePicture, {fallback: '/images/default-avatar.png'})"
     alt="Profile Picture" />

With custom MIME type:

blade
<img data-attr:src="@fileUrl($document, {defaultMime: 'image/jpeg'})"
     alt="Document Preview" />

Learn more about file handling in Forms: File Uploads.

Common Patterns

Form Submission with Validation

blade
<form @signals(['name' => '', 'email' => '', 'errors' => []])
      data-on:submit__prevent="@postx('{{ route('contacts.store') }}')">

    <div>
        <label for="name">Name</label>
        <input id="name" type="text" data-bind="name" />
        <div class="error" data-error="name"></div>
    </div>

    <div>
        <label for="email">Email</label>
        <input id="email" type="email" data-bind="email" />
        <div class="error" data-error="email"></div>
    </div>

    <button type="submit">Create Contact</button>
</form>
php
public function store()
{
    $validated = signals()->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email',
    ]);

    Contact::create($validated);

    return hyper()
        ->signals(['name' => '', 'email' => '', 'errors' => []])
        ->js("alert('Contact created!')");
}

Inline Editing with Fragments

blade
<div id="todo-{{ $todo->id }}">
    <div id="todo-label-{{ $todo->id }}">
        @fragment('todo-label')
            @if($editing)
                <input data-bind="title" value="{{ $todo->title }}" />
                <button data-on:click="@postx('{{ route('todos.update', $todo) }}')">
                    Save
                </button>
            @else
                <span>{{ $todo->title }}</span>
                <button data-on:click="@get('{{ route('todos.edit', $todo) }}')">
                    Edit
                </button>
            @endif
        @endfragment
    </div>
</div>
php
public function edit(Todo $todo)
{
    return hyper()->fragment('todos.item', 'todo-label', [
        'todo' => $todo,
        'editing' => true,
    ], [
        'selector' => '#todo-label-' . $todo->id,
        'mode' => 'inner'
    ]);
}

public function update(Todo $todo)
{
    $validated = signals()->validate(['title' => 'required|string|max:255']);

    $todo->update($validated);

    return hyper()->fragment('todos.item', 'todo-label', [
        'todo' => $todo->fresh(),
        'editing' => false,
    ], [
        'selector' => '#todo-label-' . $todo->id,
        'mode' => 'inner'
    ]);
}

Progressive Enhancement with @ifhyper

blade
<x-layouts.app>
    @ifhyper
        <!-- Minimal layout for reactive requests -->
        @fragment('page-content')
        <div id="page">
            @yield('content')
        </div>
        @endfragment
    @else
        <!-- Full layout for page loads -->
        <nav>
            <a href="{{ route('home') }}" data-navigate="true">Home</a>
            <a href="{{ route('about') }}" data-navigate="true">About</a>
        </nav>

        <div id="page">
            @yield('content')
        </div>

        <footer>
            © 2024 My Application
        </footer>
    @endifhyper
</x-layouts.app>

Dynamic Lists with Spread Syntax

blade
@foreach($contacts as $contact)
    <div id="contact-{{ $contact->id }}"
         @signals(...$contact, $_editing)>

        <div data-show="!$_editing">
            <h3 data-text="$name"></h3>
            <p data-text="$email"></p>
            <button data-on:click="$_editing = true">Edit</button>
        </div>

        <div data-show="$_editing">
            <input data-bind="name" />
            <input data-bind="email" />
            <button data-on:click="@putx('{{ route('contacts.update', $contact) }}')">
                Save
            </button>
        </div>
    </div>
@endforeach

What's Next?

Now that you understand Hyper's Blade integration, explore related topics: