Loading...
Loading...
Symfony UX frontend stack combining Stimulus, Turbo, TwigComponent and LiveComponent. Use when building modern Symfony frontends, choosing between UX tools, creating interactive components, handling real-time updates, or integrating multiple UX packages. Triggers - symfony ux, hotwire symfony, stimulus turbo, live component, twig component, frontend symfony, interactive ui, real-time symfony, which ux package, which tool should I use, how to make this interactive, SPA feel, reactive component, server-rendered component. Also trigger when the user asks a general question about frontend architecture in Symfony or wants to combine multiple UX packages together.
npx skill4agent add smnandre/symfony-ux-skills symfony-uxNeed frontend interactivity?
|
+-- Pure JavaScript behavior (no server)?
| -> Stimulus
| (DOM manipulation, event handling, third-party libs)
|
+-- Navigation / partial page updates?
| -> Turbo
| +-- Full page AJAX -> Turbo Drive (automatic, zero config)
| +-- Single section update -> Turbo Frame
| +-- Multiple sections -> Turbo Stream
|
+-- Reusable UI component?
| |
| +-- Static (no live updates)?
| | -> TwigComponent
| | (props, blocks, computed properties)
| |
| +-- Dynamic (re-renders on interaction)?
| -> LiveComponent
| (data binding, actions, forms, real-time validation)
|
+-- Real-time (WebSocket/SSE)?
-> Turbo Stream + Mercure| Feature | Stimulus | Turbo | TwigComponent | LiveComponent |
|---|---|---|---|---|
| JavaScript required | Yes (minimal) | No | No | No |
| Server re-render | No | Yes (page/frame) | No | Yes (AJAX) |
| State management | JS only | URL/Server | Props (immutable) | LiveProp (mutable) |
| Two-way binding | Manual | No | No | data-model |
| Real-time capable | Manual | Yes (Streams+Mercure) | No | Yes (polling/emit) |
| Lazy loading | Yes (stimulusFetch) | Yes (lazy frames) | No | Yes (defer/lazy) |
# All core packages
composer require symfony/ux-turbo symfony/stimulus-bundle \
symfony/ux-twig-component symfony/ux-live-component
# Individual
composer require symfony/stimulus-bundle # Stimulus
composer require symfony/ux-turbo # Turbo
composer require symfony/ux-twig-component # TwigComponent
composer require symfony/ux-live-component # LiveComponent (includes TwigComponent)#[AsTwigComponent]
final class Alert
{
public string $type = 'info';
public string $message;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
{{ message }}
</div><twig:Alert type="success" message="Saved!" />#[AsTwigComponent]
final class Dropdown
{
public string $label;
}{# templates/components/Dropdown.html.twig #}
<div data-controller="dropdown" {{ attributes }}>
<button data-action="click->dropdown#toggle">{{ label }}</button>
<div data-dropdown-target="menu" hidden>
{% block content %}{% endblock %}
</div>
</div>#[AsLiveComponent]
final class SearchBox
{
use DefaultActionTrait;
#[LiveProp(writable: true, url: true)]
public string $query = '';
public function __construct(
private readonly ProductRepository $products,
) {}
public function getResults(): array
{
return $this->products->search($this->query);
}
}<div {{ attributes }}>
<input data-model="debounce(300)|query" placeholder="Search...">
<div data-loading="addClass(opacity-50)">
{% for item in this.results %}
<div>{{ item.name }}</div>
{% endfor %}
</div>
</div><turbo-frame id="product-list">
{% for product in products %}
<a href="{{ path('product_show', {id: product.id}) }}">
{{ product.name }}
</a>
{% endfor %}
</turbo-frame>#[Route('/comments', methods: ['POST'])]
public function create(Request $request): Response
{
// ... save comment
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->render('comment/create.stream.html.twig', [
'comment' => $comment,
'count' => $count,
]);
}{# create.stream.html.twig #}
<turbo-stream action="append" target="comments">
<template>{{ include('comment/_comment.html.twig') }}</template>
</turbo-stream>
<turbo-stream action="update" target="comment-count">
<template>{{ count }}</template>
</turbo-stream><twig:Turbo:Stream:Append target="comments">
{{ include('comment/_comment.html.twig') }}
</twig:Turbo:Stream:Append><turbo-frame id="search-section">
<twig:ProductSearch />
</turbo-frame>use Symfony\UX\Turbo\Attribute\Broadcast;
#[Broadcast]
class Message
{
// Entity changes broadcast automatically
}<turbo-stream-source src="{{ mercure('chat')|escape('html_attr') }}">
</turbo-stream-source>
<div id="messages">...</div>data-turbo="false"+-----------------------------------------------------+
| Page |
| +------------------------------------------------+ |
| | Turbo Drive (automatic full-page AJAX) | |
| | +------------------------------------------+ | |
| | | Turbo Frame (partial section) | | |
| | | +------------------------------------+ | | |
| | | | LiveComponent (reactive) | | | |
| | | | +------------------------------+ | | | |
| | | | | TwigComponent (static) | | | | |
| | | | | + Stimulus (JS behavior) | | | | |
| | | | +------------------------------+ | | | |
| | | +------------------------------------+ | | |
| | +------------------------------------------+ | |
| +------------------------------------------------+ |
+-----------------------------------------------------+src/
Twig/
Components/
Alert.php # TwigComponent
Button.php # TwigComponent
SearchBox.php # LiveComponent
ProductForm.php # LiveComponent
templates/
components/
Alert.html.twig
Button.html.twig
SearchBox.html.twig
ProductForm.html.twig
assets/
controllers/
dropdown_controller.js # Stimulus
modal_controller.js # Stimulus
chart_controller.js # Stimulus<twig:Turbo:Stream:*>