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:
<!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:
<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
<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:
@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 signalvariable(sent to server)$_variable→ Creates local signal_variable(stays in browser, never sent to server)$variable_→ Creates locked signalvariable_(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:
@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:
@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:
<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:
<form @signals(...$user)>
<!-- All model attributes become signals -->
<input data-bind="name" />
<input data-bind="email" />
</form>Collections:
<div @signals(['tags' => $tagCollection])>
<!-- Collection converted to JavaScript array -->
<template data-for="tag in $tags">
<span data-text="tag.name"></span>
</template>
</div>Paginators:
<div @signals(['users' => $usersPaginated])>
<!-- Paginator converted to array of items -->
</div>Carbon Dates:
<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:
<!-- 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):
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:
- Looks for an element with
id="todo-list"(matching the root element's ID in the fragment) - Uses
outermode (default) to morph the element, preserving state like event listeners and focus - 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:
<!-- 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// 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:
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 defaultouter - Dynamic IDs: When working with model-based IDs that change per item
Example with explicit options:
<!-- 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>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:
<!-- 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>
@endfragmentRender the outer fragment to update everything, or render just the inner fragment to update the list:
// 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():
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
$editingstate - 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
<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:
// 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:
@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>
@endifhyperConditional Fragments:
Include fragment markup only when rendering for Hyper requests:
@ifhyper
@fragment('dynamic-content')
<div id="content">
{{ $content }}
</div>
@endfragment
@else
<div id="content">
{{ $content }}
</div>
@endifhyperLoading Indicators:
Show reactive loading states only for Hyper requests:
@ifhyper
<div data-show="$loading">Loading...</div>
@endifhyperDifferent Layouts:
Serve minimal layouts for reactive requests while full layouts for page loads:
@ifhyper
<!-- Minimal reactive layout without header/footer -->
@yield('content')
@else
<x-layouts.app>
@yield('content')
</x-layouts.app>
@endifhyperProgrammatic Check
You can also check programmatically in controllers using request()->isHyper():
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:
<!-- 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,@deletexfor any mutating operations - Use
@get,@post(withoutx) 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.
<input type="file" data-bind="profilePicture" accept="image/*" />
<img data-attr:src="@fileUrl($profilePicture)" alt="Profile Picture" />What it does:
- If
$profilePicturecontains a base64-encoded file (from Datastar's file input binding), it converts the array to a data URL:data:image/png;base64,... - If
$profilePictureis a file path (sent from server), it returns the path as-is:/storage/profile.jpg - If
$profilePictureis empty or null, it returns an empty string (or your custom fallback)
With fallback:
<img data-attr:src="@fileUrl($profilePicture, {fallback: '/images/default-avatar.png'})"
alt="Profile Picture" />With custom MIME type:
<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
<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>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
<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>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
<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
@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>
@endforeachWhat's Next?
Now that you understand Hyper's Blade integration, explore related topics:
- Essentials: Actions: Learn about all available Datastar and Hyper actions
- Essentials: Responses: Master the
hyper()response builder - Advanced: Locked Signals: Deep dive into secure signal handling
- Forms: File Uploads: Complete guide to handling file uploads with
@fileUrl

