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/turbo-laravel

Turbo Laravel integrates Hotwired Turbo with Laravel, offering Artisan install commands, Blade components and directives for Turbo Frames, Streams, and native apps, plus helpers that make your Laravel responses Turbo-compatible.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (10 minutes):

composer require hotwired-laravel/turbo-laravel
php artisan turbo:install

turbo:install does three things you need to verify, not trust:

  1. Publishes config/turbo.php.
  2. Registers the HandleTurboRequests middleware on the web group. On Laravel 10 this lands in app/Http/Kernel.php; on Laravel 11+ the installer will point you at bootstrap/app.php — confirm it's actually there, because without it redirects and Turbo-Frame headers behave wrong and you'll lose an hour.
  3. Optionally scaffolds view components/config for Turbo Native if you pass flags.

Version note: 2.6.0 added Laravel 13.x compatibility (the only change since 2.5.1), so the package now spans the Laravel 10 → 13 range. That's a compatibility bump, not a feature release — no code changes required on upgrade, but it does mean the installer's Laravel-11+ path is now the mainstream one, so read it carefully.

Then bring in the front end — the package does not ship Turbo itself:

npm install @hotwired/turbo
// resources/js/app.js
import '@hotwired/turbo'

Make sure your layout has the CSRF meta tag (@csrf isn't enough — Turbo needs <meta name="csrf-token" content="{{ csrf_token() }}">) and that assets are versioned, otherwise Turbo's cache serves stale HTML.

Where to look first:

  • config/turbo.php — one screen, read it all. It controls broadcasting, native path config, and the Turbo-Frame/Stream behaviors.
  • HotwiredLaravel\TurboLaravel\Facades\Turbo — this is the API surface you'll live in (Turbo::stream(), Turbo::redirectTo(), Turbo::visit(), Turbo::broadcast(), plus the isTurbo* request checks).
  • The published Blade components — <x-turbo-frame> and <x-turbo-stream> are thin wrappers around the real tags; read the source so you know what attributes pass through.
  • The docs site (turbo-laravel.com), not the README — the README is a landing page.

First use case to build: lazy-load a turbo-frame, then make a form inside it submit and answer with a turbo-stream. That's the whole mental model in one feature.

{{-- dashboard.blade.php --}}
<x-turbo-frame id="recent-orders" src="{{ route('orders.recent') }}" lazy>
    <div class="animate-pulse">Loading orders…</div>
</x-turbo-frame>
// OrderController
public function recent()
{
    return view('orders._recent', ['orders' => Order::latest()->take(10)->get()]);
}

The partial must render a <turbo-frame id="recent-orders"> at its root, and the ID must match exactly. That's the first gotcha you'll hit, so hit it deliberately.

Implementation Patterns

1. Frame-aware controllers (the daily bread). Don't build a parallel endpoint for every frame. One action, branch on the request:

public function index()
{
    $posts = Post::latest()->paginate();

    if (Turbo::isTurboFrameRequest()) {
        return view('posts._list', compact('posts'));   // slim, no layout
    }

    return view('posts.index', compact('posts'));       // full page
}

This keeps URLs shareable (a frame src is a real URL you can open in a browser) and avoids duplicate routing. Wrap the common case in a small helper or a respondWithTurbo() trait method on your base controller.

2. Turbo Streams from the controller. The package returns a real StreamedResponse with Content-Type: text/vnd.turbo-stream.html. The fluent builder is the pattern to learn:

use HotwiredLaravel\TurboLaravel\Facades\Turbo;

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

    return Turbo::stream()
        ->action('append')
        ->target('posts')
        ->partial('posts._post', ['post' => $post]);
}

Chain multiple targets for multi-region updates (append the row, replace the counter, clear the form). Reach for <x-turbo-stream> in Blade only when you need streams emitted from a full page render.

3. Redirects after non-GET. Turbo requires 303 See Other so the browser follows with a GET. The middleware converts normal redirect() calls for you, but inside frames that isn't enough — use the explicit helper when you want to break out of a frame:

return Turbo::redirectTo(route('posts.show', $post));   // 303, frame-safe
return Turbo::visit(route('posts.index'));              // full Turbo visit
return Turbo::refresh();                                // re-render current page

Rule of thumb: form submits and destructive actions → Turbo::redirectTo(); anything where the user should land somewhere new outside the frame → Turbo::visit().

4. Validation errors inside frames. Return the re-rendered frame with a 422 status — Turbo (7.1+) accepts it as a valid frame response and does not discard the page:

throw ValidationException::withMessages([...]);   // Laravel gives 422 automatically

…provided your FormRequest fails normally and the error view still renders the frame. Don't return back() from a frame; you'll get a redirect loop or a nested-frame render.

5. Broadcasting as the "live updates" layer. The package's real differentiator is Turbo::broadcast():

Turbo::broadcast($post)
    ->action('append')
    ->target('posts')
    ->partial('posts._post', ['post' => $post])
    ->toOthers();   // skips the originating tab via X-Socket-ID

This complements broadcastOn()/broadcastWith() on your Eloquent models. toOthers() only works if Laravel Echo is configured on the client (it sets the X-Socket-ID header). If Echo isn't installed, drop toOthers() — otherwise it silently no-ops and you'll chase a ghost.

6. Turbo Native (if you're building a hybrid app). The package exposes path-configuration helpers that emit the JSON your iOS/Android shells consume, plus request helpers to detect native visits. Use it to declare default vs modal vs replace_root contexts by URL pattern, and add a turbo_native_app guard so web-only routes render normally for browsers.

7. Compose with the Hotwire stack, not against it. Alpine for local UI state, Stimulus for behavior, Turbo for navigation. Keep controllers thin: streams are just responses, so extract stream-building into app/Turbo/ or a TurboStreams service class once you have more than three shapes.

Gotchas and Tips

  • The 302 trap. A plain redirect() from a POST makes Turbo re-issue the request as a GET to the redirect target, which usually works but loses flash semantics and breaks for PUT/DELETE. Always use Turbo::redirectTo() (303) for non-GET.
  • Frame IDs are a contract. append, replace, and update all target by ID. Duplicate IDs (e.g., a full page that also renders the partial inside a frame) cause writes to the wrong element. Namespace your frame/target IDs (post-42, posts, posts-count) and never reuse them across regions.
  • Streamed responses + nginx = buffered silence. If streams arrive all at once, it's not your code — it's fastcgi_buffering/gzip. Send X-Accel-Buffering: no, disable output_buffering, and make sure no middleware compresses or buffers the body. php artisan serve won't reproduce it; test behind your real web server before shipping.
  • Turbo cache is aggressive. Flash messages, modals, and "just-deleted" rows reappear on back-navigation. Mark ephemeral DOM with data-turbo-temporary, add turbo-cache-control: no-cache headers where needed, and clean up in turbo:before-cache.
  • data-turbo="false" is your escape hatch — file downloads, third-party embeds, and legacy forms that must do a full reload. It's a smaller hammer than disabling Turbo for the whole page.
  • Full-page layout in a frame response wastes bandwidth but doesn't break — Turbo extracts the matching frame from the HTML. It does break if the frame ID isn't in the response at all; Turbo surfaces "frame missing" errors that are vague in the console. Always verify the ID is present.
  • Broadcasting prerequisites: queue worker running, Echo + a transport (Pusher/Reverb) configured, config/turbo.php broadcasting enabled. Failures here are silent — no exception, just no DOM update. Add a Log::info in the broadcast callback when debugging.
  • Install command is only semi-idempotent. Re-running turbo:install can duplicate the middleware entry. Check Kernel.php/bootstrap/app.php by hand after upgrades.
  • Testing: assert on headers and streamed content, not on JavaScript. assertHeader('Turbo-Frame', 'posts') for frame requests (set it manually in tests), assertSee/assertStreamedContent for the stream body, and assert the 303 status for redirects. A Turbo::fake()-style shortcut is not something to rely on — check the package's Testing namespace for whatever macros your version ships before writing custom assertions.
  • Package maturity vs. ecosystem risk. Good conventions, MIT, stable API, and — as of 2.6.0 — declared support for Laravel 10 through 13, which is a healthy maintenance signal for a package this size. But stars and dependents: 0 mean you are still the QA team for edge cases. Pin the version in composer.json, read the changelog before bumping, and don't expect rapid fixes for weird Octane interactions. Also note: a framework-compat release like 2.6.0 is usually untested against your specific Laravel minor's new middleware/bootstrap defaults, so run your full suite after bumping — especially if you subclass HandleTurboRequests.
  • Don't layer Livewire full-page components on top of Turbo-driven navigation; both want to own the page lifecycle. If you need a reactive island, scope it to a component inside a frame and disable Turbo on that subtree.
  • Extension point worth knowing: the Turbo facade is swappable and the middleware is a normal class — subclass it if you need custom redirect rules (e.g., always breaking out of frames on a certain route prefix) rather than sprinkling conditionals in controllers.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin