Skip to content

Blade Directives

Laravel Hyper provides Blade directives that integrate reactive functionality into your Laravel views. These directives handle signal initialization, fragment definition, conditional rendering, and CSRF-protected actions.

Setup Directive

@hyper

Include Hyper's JavaScript and CSRF token meta tag in your layout.

Usage:

blade
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
    @hyper
</head>
<body>
    <!-- Your content -->
</body>
</html>

What it does:

  1. Includes CSRF meta tag: <meta name="csrf-token" content="...">
  2. Loads Hyper JavaScript: <script type="module" src="/vendor/hyper/js/hyper.js"></script>

Note: Place this directive in your <head> section, typically in your main layout file.

Signal Directives

@signals

Create reactive signals from PHP data with support for variable naming conventions and spread syntax.

blade
@signals(...$arrays)

Basic Array Syntax

Create signals from an associative array:

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

Output:

html
<div data-signals='{"count":0,"message":"Hello"}'>

Variable Syntax

Use PHP variables directly - the variable name becomes the signal name:

blade
@php
    $username = 'John';        // Regular signal
    $_editing = false;         // Local signal (stays in browser)
    $userId_ = auth()->id();   // Locked signal (protected from tampering)
@endphp

<div @signals($username, $_editing, $userId_)>
    <!-- Creates signals: username, _editing, userId_ -->
</div>

Variable naming rules (literal - no transformation):

  • $variable → Creates regular signal variable from $variable
  • $_variable → Creates local signal _variable from $_variable (not sent to server)
  • $variable_ → Creates locked signal variable_ from $variable_ (encrypted, tamper-proof)

IMPORTANT: The PHP variable name must match the desired signal name exactly:

  • To create _editing signal, use $_editing variable (not $editing)
  • To create price_ signal, use $price_ variable (not $price)

Example:

blade
@php
    $name = 'Product A';
    $_showDetails = false;  // Note: Variable name includes underscore
    $price_ = 99.99;        // Note: Variable name includes underscore
@endphp

<div @signals($name, $_showDetails, $price_)>
    <h3 data-text="$name"></h3>
    <button data-on:click="$_showDetails = !$_showDetails">Toggle</button>
    <div data-show="$_showDetails">
        Price: <span data-text="$price_"></span>
    </div>
</div>

compact() Support

Use Laravel's compact() function for convenient multi-variable signals:

blade
@php
    $username = 'john_doe';
    $email = 'john@example.com';
    $role = 'admin';
@endphp

<div @signals(compact('username', 'email', 'role'))>
    <!-- Creates signals: username, email, role -->
</div>

Spread Syntax

Spread arrays or objects to create multiple signals:

blade
<div @signals(...$user)>
    <!-- If $user = ['name' => 'John', 'email' => 'john@example.com'] -->
    <!-- Creates signals: name, email -->
</div>

Spread works with any variable - underscores are preserved:

blade
@php
    $publicData = ['page' => 1, 'sort' => 'name'];
    $_uiState = ['modal' => false, 'sidebar' => true];
    $permissions_ = ['canEdit' => true, 'canDelete' => false];
@endphp

<!-- Regular signals -->
<div @signals(...$publicData)>
    <!-- Creates: page, sort -->
</div>

<!-- Local signals (from $_-prefixed variable) -->
<div @signals(...$_uiState)>
    <!-- Creates: modal, sidebar (stays in browser) -->
</div>

<!-- Locked signals (from _-suffixed variable) -->
<div @signals(...$permissions_)>
    <!-- Creates: canEdit, canDelete (protected) -->
</div>

Eloquent models:

blade
<div @signals(...$contact)>
    <!-- All model attributes become signals -->
    <h2 data-text="$name"></h2>
    <p data-text="$email"></p>
</div>

Combining Syntax Styles

Mix arrays, variables, spreads, and compact():

blade
@php
    $username = 'John';
    $_editing = false;
    $role_ = 'admin';
    $meta = ['version' => '1.0'];
@endphp

<div @signals(
    ['count' => 0],
    $username,
    $_editing,
    $role_,
    ...$meta,
    compact('username')
)>
    <!-- All combined into one data-signals attribute -->
</div>

Type Conversion

The directive automatically converts PHP types to JSON:

blade
@signals([
    'string' => 'Hello',
    'number' => 42,
    'boolean' => true,
    'array' => [1, 2, 3],
    'object' => ['key' => 'value'],
    'null' => null,
    'model' => $user,  // Eloquent model → JSON
    'collection' => $items  // Collection → JSON array
])

Fragment Directives

@fragment / @endfragment

Define reusable view sections that can be rendered independently.

blade
@fragment(string $name)
    <!-- Content -->
@endfragment

Basic usage:

blade
@fragment('todo-list')
<div id="todo-list">
    @foreach($todos as $todo)
        <x-todo-item :todo="$todo" />
    @endforeach
</div>
@endfragment

Rendering from controller:

php
return hyper()->fragment('todos.index', 'todo-list', ['todos' => $todos]);

Multiple fragments in one view:

blade
@fragment('stats')
<div id="stats">
    <h3>Statistics</h3>
    <p>Total: {{ $total }}</p>
</div>
@endfragment

@fragment('chart')
<div id="chart">
    <canvas data-bind="chartData"></canvas>
</div>
@endfragment

Nested fragments:

blade
@fragment('page')
<div id="page">
    <header>Dashboard</header>

    @fragment('content')
    <div id="content">
        <!-- Inner content -->
    </div>
    @endfragment
</div>
@endfragment
  • Fragment names must be unique within a view
  • Include an id attribute matching the fragment name for automatic targeting
  • Fragments are purely organizational—they have zero runtime overhead

Troubleshooting Fragment Errors

If you encounter "unexpected end of file" errors when using @fragment directives, run:

bash
php artisan view:clear

This clears Laravel's Blade cache and allows views to be re-compiled with fragment directives properly registered. Hyper includes automatic error recovery, but you may need to clear cache manually after installation or when deploying to new environments.

See the Fragments documentation for more details.

Conditional Directives

@ifhyper / @else / @endifhyper

Conditionally render content based on whether the request is a Hyper (AJAX) request.

blade
@ifhyper
    <!-- Rendered only for Hyper requests -->
@else
    <!-- Rendered only for full page loads -->
@endifhyper

How it works:

Checks for the Datastar-Request header sent by Hyper requests.

Common patterns:

Progressive Enhancement:

blade
@ifhyper
    {{-- Only render the fragment for AJAX requests --}}
    @fragment('product-list')
    <div id="product-list">
        @foreach($products as $product)
            <x-product-card :product="$product" />
        @endforeach
    </div>
    @endfragment
@else
    {{-- Full page with layout for initial loads --}}
    <x-layout>
        <h1>Products</h1>
        @include('products.list')
    </x-layout>
@endifhyper

Conditional Scripts:

blade
@ifhyper
    {{-- Don't reload analytics on AJAX requests --}}
@else
    <script src="https://analytics.example.com/script.js"></script>
@endifhyper

Layout Optimization:

blade
<!DOCTYPE html>
<html>
<head>
    <title>App</title>
    @hyper

    @ifhyper
        {{-- Skip expensive assets for AJAX requests --}}
    @else
        <link rel="stylesheet" href="{{ asset('css/app.css') }}">
        <script src="{{ asset('js/app.js') }}" defer></script>
    @endifhyper
</head>
<body>
    @yield('content')
</body>
</html>

Action Directives

Action directives are provided by Hyper's frontend extensions and enable CSRF-protected HTTP requests. These are used in Datastar attributes like data-on:click.

CSRF-Protected Actions (Hyper)

These directives automatically include Laravel's CSRF token with requests.

@postx

POST request with CSRF protection.

blade
@postx(string $url)

Usage:

blade
<button data-on:click="@postx('/todos')">
    Create Todo
</button>

With signals:

blade
<form @signals(['title' => '', 'description' => ''])
      data-on:submit__prevent="@postx('/todos')">
    <input data-bind="title" />
    <textarea data-bind="description"></textarea>
    <button type="submit">Save</button>
</form>

@putx

PUT request with CSRF protection.

blade
@putx(string $url)

Usage:

blade
<button data-on:click="@putx('/todos/' + $todoId)">
    Update
</button>

@patchx

PATCH request with CSRF protection.

blade
@patchx(string $url)

Usage:

blade
<button data-on:click="@patchx('/users/{{ $user->id }}')">
    Update Profile
</button>

@deletex

DELETE request with CSRF protection.

blade
@deletex(string $url)

Usage:

blade
<button data-on:click="@deletex('/todos/' + $todoId)"
        data-on:click__confirm="Are you sure?">
    Delete
</button>

CSRF Token Source:

These directives read the CSRF token from the <meta name="csrf-token"> tag included by @hyper.

File URL Action (Hyper)

@fileUrl

Convert file signals to displayable URLs with fallback support.

blade
@fileUrl(mixed $fileSource, object $options = {})

Parameters:

  • $fileSource - Signal name, file path, or base64 data
  • $options - Optional configuration object:
    • fallback - URL to use if file is missing/empty
    • defaultMime - MIME type for base64 data (default: 'application/octet-stream')
    • mimeSignal - Name of companion signal containing MIME type

Usage with file input:

blade
<div @signals(['profilePicture' => null])>
    <input type="file" data-bind="profilePicture" accept="image/*" />

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

With server file paths:

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

With custom MIME type:

blade
<img data-attr:src="@fileUrl($photo, {defaultMime: 'image/png'})"
     alt="Photo" />

How it works:

  1. Base64 arrays (from file inputs): Converted to data URLs
  2. File paths: Passed through as-is (Laravel handles URL conversion)
  3. Null/empty: Returns fallback URL
  4. Invalid data: Returns fallback URL

File signal structure:

When a user selects a file, Datastar creates:

json
{
    "profilePicture": [
        "/9j/4AAQSkZJRgABAQAAAQ..."  // Base64 string
    ]
}

Event Dispatch Action (Hyper)

@dispatch

Dispatch a custom browser event from inline Blade expressions, enabling component communication without dedicated controller endpoints.

blade
@dispatch(string $eventName, mixed $data = null, object $options = {})
blade
<!-- ✅ Correct: Escaped with @ in attributes -->
<button data-on:click="@dispatch('count-updated', {count: $count})">
    Update
</button>

For global events, listeners must use the __window modifier to catch events dispatched to the window object. :::

Parameters:

  • $eventName - Name of the event to dispatch
  • $data - Event data (available in event.detail)
  • $options - Event options:
    • selector - Target specific elements (default: global window)
    • bubbles - Whether event bubbles (default: true)
    • cancelable - Whether event is cancelable (default: true)
    • composed - Whether event crosses shadow DOM boundaries (default: true)

Basic usage in attributes:

blade
<button data-on:click="@dispatch('count-updated', {count: $count})">
    Update
</button>

With signal data:

blade
<button data-on:click="@dispatch('post-created', {id: $postId, title: $title})">
    Create Post
</button>

Targeted dispatch:

blade
<button data-on:click="@dispatch('refresh-stats', null, {selector: '#dashboard'})">
    Refresh Dashboard
</button>

Listening for dispatched events:

blade
{{-- Listen with Datastar (use __window for global events) --}}
<div data-on:count-updated__window="alert('Count: ' + event.detail.count)">
    Listening...
</div>

{{-- Listen with JavaScript --}}
<script>
window.addEventListener('count-updated', (event) => {
    console.log('New count:', event.detail.count);
});
</script>

Complete example:

blade
<div @signals(['count' => 0])>
    <button data-on:click="$count = $count + 1; @dispatch('count-updated', {count: $count})">
        Increment
    </button>

    <div data-on:count-updated__window="console.log('Count changed:', event.detail.count)">
        Count: <span data-text="$count"></span>
    </div>
</div>

Use cases:

  • Notify other components of state changes
  • Trigger animations or UI updates in response to actions
  • Coordinate between independent page sections
  • Implement real-time updates without polling

Server-side alternative:

For server-triggered events, use hyper()->dispatch() in controllers:

php
public function store(Request $request)
{
    $post = Post::create($request->validated());

    return hyper()
        ->signals(['posts' => Post::all()])
        ->dispatch('post-created', ['id' => $post->id]);
}

Standard Datastar Actions

These actions are provided by Datastar (not Hyper) and don't include CSRF protection. Use them for GET requests or non-mutating operations.

@get

blade
<button data-on:click="@get('/api/data')">Fetch Data</button>

@post

blade
<!-- ⚠️ No CSRF protection - use @postx for Laravel routes -->
<button data-on:click="@post('/external-api')">Post</button>

@put, @patch, @delete

blade
<!-- ⚠️ No CSRF protection - use @putx, @patchx, @deletex for Laravel routes -->

For Laravel routes that modify data, always use the x variants (@postx, @putx, @patchx, @deletex).

Common Patterns

Complete Form Example

blade
<div @signals([
    'name' => $contact->name ?? '',
    'email' => $contact->email ?? '',
    'phone' => $contact->phone ?? '',
    'errors' => []
])>
    <form data-on:submit__prevent="@postx('/contacts')">
        <div>
            <label>Name</label>
            <input data-bind="name" />
            <div data-error="name"></div>
        </div>

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

        <div>
            <label>Phone</label>
            <input data-bind="phone" type="tel" />
            <div data-error="phone"></div>
        </div>

        <button type="submit">Save Contact</button>
    </form>
</div>

Fragment with Progressive Enhancement

blade
@fragment('search-results')
<div id="search-results">
    @if($products->isEmpty())
        <p>No products found.</p>
    @else
        @foreach($products as $product)
            <x-product-card :product="$product" />
        @endforeach
    @endif
</div>
@endfragment

@ifhyper
    {{-- AJAX request: only render fragment --}}
@else
    {{-- Full page load: include layout --}}
    <x-layout>
        <h1>Search Results</h1>
        @include('products.search-form')
        @include('products.search-results')
    </x-layout>
@endifhyper

File Upload with Preview

blade
<div @signals(['avatar' => null, 'errors' => []])>
    <form data-on:submit__prevent="@postx('/profile/avatar')">
        <div>
            <label>Profile Picture</label>
            <input type="file"
                   data-bind="avatar"
                   accept="image/*" />
            <div data-error="avatar"></div>
        </div>

        <div data-show="$avatar">
            <img data-attr:src="@fileUrl($avatar, {fallback: '/default-avatar.png'})"
                 alt="Preview"
                 class="preview-image" />
        </div>

        <button type="submit" data-show="$avatar">
            Upload
        </button>
    </form>
</div>

Locked Signals for Security

IMPORTANT: Use literal variable names with underscores for locked signals:

blade
@php
    // Create PHP variables WITH trailing underscores
    $isAdmin_ = auth()->user()->isAdmin();
    $userId_ = auth()->id();
@endphp

<div @signals($userId_, $isAdmin_)>
    <h2>User Management</h2>

    @foreach($users as $user)
        <div>
            <span>{{ $user->name }}</span>
            <button data-on:click="@deletex('/users/{{ $user->id }}')"
                    data-show="$isAdmin_ && $userId_ !== {{ $user->id }}">
                Delete
            </button>
        </div>
    @endforeach
</div>

Alternative using array syntax:

blade
@php
    // Regular PHP variables
    $isAdmin = auth()->user()->isAdmin();
    $userId = auth()->id();
@endphp

<div @signals(['userId_' => $userId, 'isAdmin_' => $isAdmin])>
    <!-- Manually specify locked signal names with _ suffix -->
</div>
php
// In Controller
public function destroy(User $user)
{
    $isAdmin = signals('isAdmin_');  // Validated automatically
    $userId = signals('userId_');    // Cannot be tampered with

    if (!$isAdmin) {
        abort(403);
    }

    if ($userId === $user->id) {
        abort(400, 'Cannot delete yourself');
    }

    $user->delete();

    return hyper()->signals(['message' => 'User deleted']);
}

Dynamic Content Updates

blade
@fragment('notification-count')
<span id="notification-count"
      data-class:badge="$notificationCount > 0"
      data-text="$notificationCount > 0 ? $notificationCount : ''">
</span>
@endfragment

<button data-on:click="@deletex('/notifications/' + $id)">
    Dismiss
</button>