--- name: turbo description: Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (full-page navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded page sections, inline editing, pagination without reload, modals loaded from the server, flash messages via streams, real-time updates via Mercure/SSE, or multi-section page updates. Code triggers: turbo-frame, turbo-stream, data-turbo-frame, data-turbo, data-turbo-action, turbo-stream-source, TurboStreamResponse, , , , turbo:before-fetch-request. Also trigger when the user asks "how to update part of the page without reload", "how to make navigation feel like SPA", "how to lazy-load a section", "how to do inline editing", "how to push real-time updates from server", "how to use Mercure with Turbo". Do NOT trigger for client-side JS behavior (use stimulus), server-rendered reactive components (use live-component), or reusable static UI (use twig-component). license: MIT metadata: author: Simon Andre email: smn.andre@gmail.com url: https://smnandre.dev version: "1.0" --- # Turbo Hotwire Turbo provides SPA-like speed with server-rendered HTML. No JavaScript to write. Three components work together: - **Drive** -- Automatic AJAX navigation for all links and forms (zero config) - **Frames** -- Scoped navigation that updates only one section of the page - **Streams** -- Server-pushed DOM mutations (append, replace, remove, etc.) ## Decision Tree ``` Need to update the page? +-- Full page navigation -> Turbo Drive (automatic, already active) +-- Single section from user click -> Turbo Frame +-- Multiple sections from action -> Turbo Stream (HTTP response) +-- Real-time from server/others -> Turbo Stream (Mercure / SSE) ``` ## Installation ```bash composer require symfony/ux-turbo ``` That's it. Turbo Drive is active immediately -- all links and forms become AJAX. ## Turbo Drive Automatic SPA-like navigation. Every `` click and `
` submit is intercepted, fetched via AJAX, and the `` is swapped. The browser URL and history update normally. ### Disabling for Specific Elements ```html External Link
Normal link (no Turbo)
``` ### History and Caching ```html Replace History ``` ## Turbo Frames Scope navigation to a section of the page. Links and forms inside a frame update only that frame's content. The rest of the page stays untouched. ### Basic Frame ```html

Messages

View Message 1

Message 1

Content here...

Back to list
``` The server response is a full HTML page, but Turbo extracts only the matching `` and swaps it in. ### Lazy Loading Load frame content asynchronously after the page renders: ```html

Loading...

``` ### Target Another Frame A link inside one frame can update a different frame: ```html View Item ``` ### Break Out of Frame Navigate the entire page from within a frame: ```html Go to Dashboard ``` ### Frame with Form Forms inside frames submit and update within that frame: ```html
    {% for item in results %}
  • {{ item.name }}
  • {% endfor %}
``` ### URL Sync Update the browser URL when a frame navigates (useful for bookmarkable state): ```html ``` ## Turbo Streams Update multiple DOM elements from a single server response. Eight actions available (`append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`), each targeting elements by ID or CSS selector. ### Stream Actions ```html ``` `replace` and `update` support an optional `method="morph"` attribute for smooth DOM morphing instead of full replacement: ```html ``` ### Target Multiple Elements (CSS Selector) Use `targets` (plural) with a CSS selector to affect multiple elements: ```html ``` ### Twig Component Syntax for Streams Since Symfony UX 2.22+, you can use `` components instead of raw HTML: ```twig {{ include('comment/_comment.html.twig') }} {{ count }} ``` ## Symfony Integration ### Stream Response from Controller ```php use Symfony\UX\Turbo\TurboBundle; #[Route('/messages', name: 'message_create', methods: ['POST'])] public function create(Request $request): Response { $message = new Message(); // ... handle form $this->em->persist($message); $this->em->flush(); // Return stream response for Turbo requests $request->setRequestFormat(TurboBundle::STREAM_FORMAT); return $this->render('message/create.stream.html.twig', [ 'message' => $message, 'count' => $count, ]); } ``` You can also use the `TurboStreamResponse` helper or `TurboStream` helper methods for programmatic stream building. ### Stream Template ```twig {# templates/message/create.stream.html.twig #} ``` ### Detect Frame Request ```php public function show(Request $request, int $id): Response { if ($request->headers->has('Turbo-Frame')) { $frameId = $request->headers->get('Turbo-Frame'); // Return only the frame content (or a full page -- Turbo extracts the frame) } return $this->render('page/show.html.twig'); } ``` ### Mercure Broadcasts (Real-time) Push changes to all connected browsers via SSE: ```php use Symfony\UX\Turbo\Attribute\Broadcast; #[Broadcast] class Message { // Entity changes broadcast automatically to subscribed clients } ``` ```twig {# Subscribe to Mercure topic #}
{# Messages appear here in real-time #}
``` ## Common Patterns ### Inline Edit ```html {{ task.title }} Edit
Cancel
``` ### Modal in Frame ```html Delete

Confirm delete?

Cancel
``` ### Flash Messages with Stream ```twig ``` ## Key Principles **Server returns full HTML pages.** Turbo works best when the server always returns a complete, valid HTML page. Turbo Drive replaces the body, Turbo Frames extract the matching frame. Don't try to return partial HTML snippets (except for Stream templates). **Frame IDs must match.** The frame in the response must have the same `id` as the frame on the page. If they don't match, Turbo shows an error. **Streams are for side effects.** Use Streams when a single action needs to update multiple unrelated parts of the page. If you're only updating one section, a Frame is simpler. **Stimulus complements Turbo.** Turbo handles navigation and server communication. Stimulus handles client-side behavior (animations, toggles, clipboard). They work together -- Stimulus controllers survive Turbo Frame swaps within their scope, and reconnect properly on Drive navigation. ## References - **Full API** (Drive events, Frame attributes, Stream actions, Mercure): [references/api.md](references/api.md) - **Patterns** (forms, modals, search, pagination, infinite scroll): [references/patterns.md](references/patterns.md) - **Gotchas** (caching issues, form handling, Stimulus integration): [references/gotchas.md](references/gotchas.md) ## See Also - **UX Map** works inside Turbo Frames. The map Stimulus controller reconnects properly on frame swaps. - **UX Icons** are inline SVG and survive Turbo Drive navigation and Frame swaps with no special handling.