Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Turbo Laravel Laravel Package

hotwired-laravel/turbo-laravel

Turbo Laravel brings Hotwire Turbo to Laravel, delivering SPA-like speed with server-rendered HTML. It integrates Turbo Drive, Frames, and Streams with Blade helpers and page morphing, so you can build fast, reactive interfaces without writing custom JavaScript.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire-up (5 minutes):

composer require hotwired-laravel/turbo-laravel:^2.6
php artisan turbo:install          # publishes config, layout scaffolding, JS entrypoint
npm install && npm run dev

Then in resources/js/app.js:

import '@hotwired/turbo-laravel'

Two non-negotiables before anything works:

  1. <meta name="csrf-token" content="{{ csrf_token() }}"> in your layout <head> — turbo:install adds it, but custom layouts often lose it and every non-GET request 419s.
  2. @hotwired/turbo must be loaded via the package's JS package (it re-exports/initialises Turbo), not imported twice.

Where to look first:

  • config/turbo.php — the only file you'll actually edit; skim it before writing code so you don't hardcode what's configurable.
  • The HotwiredLaravel\TurboLaravel\Facades\Turbo facade — it's thin, and reading it tells you the whole API surface faster than the docs.
  • vendor/hotwired-laravel/turbo-laravel/src/Http/ for the response classes and testing assertions — these define the contract you'll be asserting against.

First real use case: take an existing PostsController@store that does return redirect()->route('posts.index') and make the new row appear without a page reload:

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

    return Turbo::stream('append', target: 'posts', partial: 'posts._post', data: compact('post'))
        ->toResponse($request);
}
<div id="posts">@foreach ($posts as $post) @include('posts._post') @endforeach</div>

That single change — partial + dom_id-consistent target — is the pattern you'll repeat 90% of the time.

Implementation Patterns

Pick one ID convention on day one. Use the package's dom_id() helper everywhere (Blade and streams) so targets can't drift:

<div id="{{ dom_id($post) }}"> … </div>

Stream responses belong on non-GET requests. For navigations use frames, for mutations use streams. Mixing them (returning a stream to a Turbo Drive visit) silently does nothing — Turbo only accepts text/vnd.turbo-stream.html on form submissions and data-turbo-stream requests.

Frames for in-place navigation, streams for multi-region updates. A lazy frame is the cheapest win for pagination/tabs:

<x-turbo-frame id="post_42" src="{{ route('posts.show', $post) }}" loading="lazy">
    <p>Loading…</p>
</x-turbo-frame>

Request-type branching. Controllers that serve both HTML and frame/stream clients should branch explicitly rather than guessing:

if (Turbo::isTurboFrameRequest()) {
    return view('posts._show', compact('post')); // frame-only, no layout
}

return view('posts.show', compact('post'));

Validation errors as streams. Override failedValidation() in FormRequests (or render the ValidationException from the exception handler) to return a stream that replaces the error container and re-renders the form — status 422 + text/vnd.turbo-stream.html is what Turbo expects:

protected function failedValidation(Validator $validator)
{
    if (Turbo::isTurboRequest()) {
        throw (new ValidationException($validator))
            ->response(function ($request, $response) use ($validator) {
                return Turbo::stream('replace', target: 'form_errors', partial: 'shared._errors', data: ['errors' => $validator->errors()])
                    ->toResponse($request)
                    ->setStatusCode(422);
            });
    }

    parent::failedValidation($validator);
}

Redirects inside stream responses must go through the facade (Turbo::redirect(...), Turbo::refresh()), not redirect(). A plain RedirectResponse embedded where the client expects a stream is not followed.

Testing is first-class — use it. The package ships Turbo Stream assertions, so a stream regression is a red test rather than a manual click-through:

$response->assertOk()
    ->assertTurboStream()
    ->assertTurboStreamSee('New comment')
    ->assertTurboStreamMissing('Nothing here');

Native/mobile shell. Turbo::isTurboNativeVisit() plus the native redirect helpers let one controller serve both the web and a Turbo Native wrapper; keep that branch at the top of your auth/redirect paths so device flows don't fight web flows.

Broadcasting. Streams can be pushed over Echo for multi-user updates; treat that as a phase-two concern — the same partial renders both paths, and queued workers must be running or updates appear only for the actor.

Gotchas and Tips

  • "Content missing" in a frame = you returned a full layout to a frame request. Frame responses must be layout-free, with a root element whose id matches the frame.
  • Stale flash messages on Back. Turbo Drive caches the DOM; mark flash containers data-turbo-temporary (Turbo 8) or clear them in a turbo:before-cache listener. This is the #1 "is the package broken?" report.
  • Full-page redirects after form submits work fine, but they convert to GET — don't rely on the original method surviving. Use Turbo::redirect() for stream-context redirects.
  • File downloads and OAuth hops need data-turbo="false" on the link/form, otherwise the response gets swallowed into the page.
  • Debugging: check the response Content-Type first, then the target id in the DOM, then the network tab for Turbo-Frame request headers. Nine times out of ten it's an id typo or a layout leaking into a frame.
  • Assertions are HTML-fragment based, not JSON — don't reach for assertJson; and if you're comparing rendered markup, normalise whitespace or the test becomes brittle.
  • Configuration/extension points: keep config/turbo.php under version control once edited; the Turbo facade is container-resolved, so a custom implementation can be bound in a service provider for multi-tenant or shell-specific redirect logic, and the facade is trivially mockable in unit tests.
  • Upgrade note for 2.6.0: this release only adds Laravel 13.x compatibility — no API changes, no deprecations, no config changes. Safe to bump within ^2.6 on Laravel 10–13. On a fresh L13 app, remember middleware registration moved to bootstrap/app.php; the turbo:install output generated against older skeletons may need that file hand-edited.
  • Pin the version in CI (^2.6) and run the JS build in the pipeline — server-side stream tests pass happily while the front-end package is unmounted, which produces "it works in tests, not in the browser" confusion.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator