/turbo
Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded sections, inline editing, pagination without reload, modals from the
$ npx -y skills add smnandre/symfony-ux-skills --skill turbo --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/turbo
Context preview
The summary Claude sees to decide when to auto-load this skill.
Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded sections, inline editing, pagination without reload, modals from the
SKILL.md
turbo.SKILL.mdname: turbo
description: 'Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded sections, inline editing, pagination without reload, modals from the server, flash messages via streams, or real-time updates via Mercure/SSE. Code triggers: turbo-frame, turbo-stream, data-turbo-frame, data-turbo, data-turbo-action, turbo-stream-source, TurboStreamResponse, <twig:Turbo:Frame>, <twig:Turbo:Stream:Append>, <twig:Turbo:Stream:Replace>, 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.2.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
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 `<a>` click and `<form>` submit is intercepted, fetched via AJAX, and the `<body>` is swapped. The browser URL and history update normally.
Disabling for Specific Elements
<!-- Disable on link/form -->
<a href="/external" data-turbo="false">External Link</a>
<!-- Disable for entire section -->
<div data-turbo="false">
<a href="/normal">Normal link (no Turbo)</a>
</div>History and Caching
<!-- Replace history instead of push -->
<a href="/page" data-turbo-action="replace">Replace History</a>
<!-- Force full reload when asset changes -->
<link rel="stylesheet" href="/app.css" data-turbo-track="reload">
<script src="/app.js" data-turbo-track="reload"></script>
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
<!-- Page with frame -->
<turbo-frame id="messages">
<h2>Messages</h2>
<a href="/messages/1">View Message 1</a> <!-- Updates only this frame -->
</turbo-frame>
<!-- /messages/1 response must contain a matching frame ID -->
<turbo-frame id="messages">
<h2>Message 1</h2>
<p>Content here...</p>
<a href="/messages">Back to list</a>
</turbo-frame>The server response is a full HTML page, but Turbo extracts only the matching `<turbo-frame>` and swaps it in.
Lazy Loading
Load frame content asynchronously after the page renders:
<turbo-frame id="notifications" src="/notifications" loading="lazy">
<p>Loading...</p>
</turbo-frame>Target Another Frame
A link inside one frame can update a different frame:
<turbo-frame id="sidebar">
<a href="/item/1" data-turbo-frame="main-content">View Item</a>
</turbo-frame>
<turbo-frame id="main-content">
<!-- Content replaced here -->
</turbo-frame>Break Out of Frame
Navigate the entire page from within a frame:
<turbo-frame id="modal">
<a href="/dashboard" data-turbo-frame="_top">Go to Dashboard</a>
</turbo-frame>Frame with Form
Forms inside frames submit and update within that frame:
<turbo-frame id="search-results">
<form action="/search" method="get">
<input type="search" name="q">
<button>Search</button>
</form>
<ul>
{% for item in results %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
</turbo-frame>URL Sync
Update the browser URL when a frame navigates (useful for bookmarkable state):
<turbo-frame id="products" data-turbo-action="advance">
<!-- Browser URL updates when this frame navigates -->
</turbo-frame>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
<turbo-stream action="append" target="messages">
<template><div id="msg_1">New message</div></template>
</turbo-stream>
<turbo-stream action="prepend" target="messages">
<template><div id="msg_0">First!</div></template>
</turbo-stream>
<turbo-stream action="replace" target="notification">
<template><div id="notification">Updated!</div></template>
</turbo-stream>
<turbo-stream action="update" target="counter">
<template>42</template>
</turbo-stream>
<turbo-stream action="remove" target="msg_5"></turbo-stream>
<turbo-stream action="before" target="msg_3">
<template><div id="msg_2">Inserted before</div></template>
</turbo-stream>
<turbo-stream action="after" target="msg_3">
<template><div id="msg_4">Inserted after</div></template>
</turbo-stream>
<turbo-stream action="refresh"></turbo-stream>`replace` and `update` support an optional `method="morph"` attribute for smooth DOM morphing instead of full replacement:
<turbo-stream action="replace" method="morph" target="
Read more
name: turbo description: 'Hotwire Turbo for Symfony UX -- SPA-like speed with zero JavaScript. Covers Drive (navigation), Frames (partial page sections), and Streams (multi-target updates). Use when building ajax navigation, lazy-loaded sections, inline editing, pagination without reload, modals from the server, flash messages via streams, or real-time updates via Mercure/SSE. Code triggers: turbo-frame, turbo-stream, data-turbo-frame, data-turbo, data-turbo-action, turbo-stream-source, TurboStreamResponse, <twig:Turbo:Frame>, <twig:Turbo:Stream:Append>, <twig:Turbo:Stream:Replace>, 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.2.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
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 `<a>` click and `<form>` submit is intercepted, fetched via AJAX, and the `<body>` is swapped. The browser URL and history update normally.
Disabling for Specific Elements
<!-- Disable on link/form -->
<a href="/external" data-turbo="false">External Link</a>
<!-- Disable for entire section -->
<div data-turbo="false">
<a href="/normal">Normal link (no Turbo)</a>
</div>History and Caching
<!-- Replace history instead of push --> <a href="/page" data-turbo-action="replace">Replace History</a> <!-- Force full reload when asset changes --> <link rel="stylesheet" href="/app.css" data-turbo-track="reload"> <script src="/app.js" data-turbo-track="reload"></script>
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
<!-- Page with frame -->
<turbo-frame id="messages">
<h2>Messages</h2>
<a href="/messages/1">View Message 1</a> <!-- Updates only this frame -->
</turbo-frame>
<!-- /messages/1 response must contain a matching frame ID -->
<turbo-frame id="messages">
<h2>Message 1</h2>
<p>Content here...</p>
<a href="/messages">Back to list</a>
</turbo-frame>The server response is a full HTML page, but Turbo extracts only the matching `<turbo-frame>` and swaps it in.
Lazy Loading
Load frame content asynchronously after the page renders:
<turbo-frame id="notifications" src="/notifications" loading="lazy">
<p>Loading...</p>
</turbo-frame>Target Another Frame
A link inside one frame can update a different frame:
<turbo-frame id="sidebar">
<a href="/item/1" data-turbo-frame="main-content">View Item</a>
</turbo-frame>
<turbo-frame id="main-content">
<!-- Content replaced here -->
</turbo-frame>Break Out of Frame
Navigate the entire page from within a frame:
<turbo-frame id="modal">
<a href="/dashboard" data-turbo-frame="_top">Go to Dashboard</a>
</turbo-frame>Frame with Form
Forms inside frames submit and update within that frame:
<turbo-frame id="search-results">
<form action="/search" method="get">
<input type="search" name="q">
<button>Search</button>
</form>
<ul>
{% for item in results %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
</turbo-frame>URL Sync
Update the browser URL when a frame navigates (useful for bookmarkable state):
<turbo-frame id="products" data-turbo-action="advance">
<!-- Browser URL updates when this frame navigates -->
</turbo-frame>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
<turbo-stream action="append" target="messages">
<template><div id="msg_1">New message</div></template>
</turbo-stream>
<turbo-stream action="prepend" target="messages">
<template><div id="msg_0">First!</div></template>
</turbo-stream>
<turbo-stream action="replace" target="notification">
<template><div id="notification">Updated!</div></template>
</turbo-stream>
<turbo-stream action="update" target="counter">
<template>42</template>
</turbo-stream>
<turbo-stream action="remove" target="msg_5"></turbo-stream>
<turbo-stream action="before" target="msg_3">
<template><div id="msg_2">Inserted before</div></template>
</turbo-stream>
<turbo-stream action="after" target="msg_3">
<template><div id="msg_4">Inserted after</div></template>
</turbo-stream>
<turbo-stream action="refresh"></turbo-stream>`replace` and `update` support an optional `method="morph"` attribute for smooth DOM morphing instead of full replacement:
<turbo-stream action="replace" method="morph" target="
Showing the first part of this file.
AI agent skills for the Symfony UX frontend stack -- Stimulus, Turbo, TwigComponent, LiveComponent, UX Icons and UX Map. By Simon Andre
Repo: smnandre/symfony-ux-skills
Other skills on symfony-ux-skills.
- /live-component
Symfony UX LiveComponent for reactive server-rendered UI -- components that re-render via AJAX on user interaction, zero JavaScript required. Use when building live search, real-time filtering, dynamic forms, inline validation, dependent selects, auto-save, polling,
Open skill - /stimulus
Stimulus JS framework for Symfony UX -- client-side behavior via HTML data attributes, zero server round-trips. Use when creating controllers for DOM manipulation, handling click/input/submit events, managing targets and values, wiring outlets between controllers, wrapping
Open skill - /symfony-ux
Symfony UX frontend stack -- decision tree and orchestrator for choosing between Stimulus, Turbo, TwigComponent, LiveComponent, UX Icons, and UX Map. Use when the user is unsure which tool fits, wants to combine multiple UX packages, or asks a general frontend architecture
Open skill - /twig-component
Symfony UX TwigComponent for reusable UI building blocks -- server-rendered components with PHP classes and Twig templates. Use when creating buttons, cards, alerts, badges, navbars, or any reusable UI element with props, blocks/slots, computed properties, or anonymous
Open skill - /ux-icons
Symfony UX Icons for rendering SVG icons in Twig templates. Supports 200,000+ Iconify icons (Lucide, Heroicons, Tabler, Material Design, etc.), local SVG files, and custom icon sets with aliases. Use when displaying icons, configuring icon defaults, importing or locking
Open skill - /ux-map
Symfony UX Map for interactive maps with Leaflet or Google Maps in Symfony. Covers markers, polygons, polylines, circles, info windows, and LiveComponent integration. Use when displaying maps, placing markers, drawing shapes or routes, handling map events, building store
Open skill

