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:
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
@hyper
</head>
<body>
<!-- Your content -->
</body>
</html>What it does:
- Includes CSRF meta tag:
<meta name="csrf-token" content="..."> - 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.
@signals(...$arrays)Basic Array Syntax
Create signals from an associative array:
<div @signals(['count' => 0, 'message' => 'Hello'])>
<p data-text="$count"></p>
<p data-text="$message"></p>
</div>Output:
<div data-signals='{"count":0,"message":"Hello"}'>Variable Syntax
Use PHP variables directly - the variable name becomes the signal name:
@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 signalvariablefrom$variable$_variable→ Creates local signal_variablefrom$_variable(not sent to server)$variable_→ Creates locked signalvariable_from$variable_(encrypted, tamper-proof)
IMPORTANT: The PHP variable name must match the desired signal name exactly:
- To create
_editingsignal, use$_editingvariable (not$editing) - To create
price_signal, use$price_variable (not$price)
Example:
@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:
@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:
<div @signals(...$user)>
<!-- If $user = ['name' => 'John', 'email' => 'john@example.com'] -->
<!-- Creates signals: name, email -->
</div>Spread works with any variable - underscores are preserved:
@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:
<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():
@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:
@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.
@fragment(string $name)
<!-- Content -->
@endfragmentBasic usage:
@fragment('todo-list')
<div id="todo-list">
@foreach($todos as $todo)
<x-todo-item :todo="$todo" />
@endforeach
</div>
@endfragmentRendering from controller:
return hyper()->fragment('todos.index', 'todo-list', ['todos' => $todos]);Multiple fragments in one view:
@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>
@endfragmentNested fragments:
@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
idattribute 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:
php artisan view:clearThis 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.
@ifhyper
<!-- Rendered only for Hyper requests -->
@else
<!-- Rendered only for full page loads -->
@endifhyperHow it works:
Checks for the Datastar-Request header sent by Hyper requests.
Common patterns:
Progressive Enhancement:
@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>
@endifhyperConditional Scripts:
@ifhyper
{{-- Don't reload analytics on AJAX requests --}}
@else
<script src="https://analytics.example.com/script.js"></script>
@endifhyperLayout Optimization:
<!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.
@postx(string $url)Usage:
<button data-on:click="@postx('/todos')">
Create Todo
</button>With signals:
<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.
@putx(string $url)Usage:
<button data-on:click="@putx('/todos/' + $todoId)">
Update
</button>@patchx
PATCH request with CSRF protection.
@patchx(string $url)Usage:
<button data-on:click="@patchx('/users/{{ $user->id }}')">
Update Profile
</button>@deletex
DELETE request with CSRF protection.
@deletex(string $url)Usage:
<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.
@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/emptydefaultMime- MIME type for base64 data (default:'application/octet-stream')mimeSignal- Name of companion signal containing MIME type
Usage with file input:
<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:
<img data-attr:src="@fileUrl($user->avatar, {fallback: '/default.png'})"
alt="Avatar" />With custom MIME type:
<img data-attr:src="@fileUrl($photo, {defaultMime: 'image/png'})"
alt="Photo" />How it works:
- Base64 arrays (from file inputs): Converted to data URLs
- File paths: Passed through as-is (Laravel handles URL conversion)
- Null/empty: Returns fallback URL
- Invalid data: Returns fallback URL
File signal structure:
When a user selects a file, Datastar creates:
{
"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.
@dispatch(string $eventName, mixed $data = null, object $options = {})<!-- ✅ 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 inevent.detail)$options- Event options:selector- Target specific elements (default: globalwindow)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:
<button data-on:click="@dispatch('count-updated', {count: $count})">
Update
</button>With signal data:
<button data-on:click="@dispatch('post-created', {id: $postId, title: $title})">
Create Post
</button>Targeted dispatch:
<button data-on:click="@dispatch('refresh-stats', null, {selector: '#dashboard'})">
Refresh Dashboard
</button>Listening for dispatched events:
{{-- 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:
<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:
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
<button data-on:click="@get('/api/data')">Fetch Data</button>@post
<!-- ⚠️ No CSRF protection - use @postx for Laravel routes -->
<button data-on:click="@post('/external-api')">Post</button>@put, @patch, @delete
<!-- ⚠️ 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
<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
@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>
@endifhyperFile Upload with Preview
<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:
@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:
@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>// 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
@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>Related Documentation
- Signals - Understanding signals
- Blade Integration - Integration patterns
- HyperResponse - Server response methods
- Signals Helper - Server-side signal access
- Hyper Attributes - Custom attributes reference

