Loading...
Loading...
Symfony UX TwigComponent for reusable UI elements. Use when creating reusable Twig templates with PHP backing classes, component composition, props, slots/blocks, computed properties, or anonymous components. Triggers - twig component, AsTwigComponent, reusable template, component props, twig blocks, component slots, anonymous component, Symfony UX component, HTML component, component library, design system component, UI kit, reusable button, reusable card, PreMount, PostMount, mount method. Also trigger for any question about building a reusable piece of UI in Symfony, even if the user doesn't mention TwigComponent by name.
npx skill4agent add smnandre/symfony-ux-skills twig-componentcomposer require symfony/ux-twig-component#[AsTwigComponent]// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
final class Alert
{
public string $type = 'info';
public string $message;
public bool $dismissible = false;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}" {{ attributes }}>
{{ message }}
{% if dismissible %}
<button type="button" class="close">×</button>
{% endif %}
</div>{# Usage #}
<twig:Alert type="success" message="Saved!" />
<twig:Alert type="danger" message="Error occurred" dismissible />
{# With block content instead of message prop #}
<twig:Alert type="warning">
<strong>Warning:</strong> Check your input
</twig:Alert>{% props %}{# templates/components/Button.html.twig #}
{% props variant = 'primary', size = 'md', disabled = false %}
<button
class="btn btn-{{ variant }} btn-{{ size }}"
{{ disabled ? 'disabled' }}
{{ attributes }}
>
{% block content %}{% endblock %}
</button><twig:Button variant="danger" size="lg">Delete</twig:Button>#[AsTwigComponent]
final class Card
{
public string $title; // Required
public ?string $subtitle = null; // Optional
public bool $shadow = true; // Optional with default
}mount()#[AsTwigComponent]
final class UserCard
{
public User $user;
public string $displayName;
public function mount(User $user): void
{
$this->user = $user;
$this->displayName = $user->getFullName();
}
}<twig:UserCard :user="currentUser" />:{# Pass a variable #}
<twig:Alert :type="alertType" :message="flashMessage" />
{# Pass an expression #}
<twig:UserList :users="users|filter(u => u.active)" />{% block content %}{# Component template #}
<div class="card">{% block content %}{% endblock %}</div>
{# Usage #}
<twig:Card><p>This is the card content</p></twig:Card>{# templates/components/Modal.html.twig #}
<dialog class="modal" {{ attributes }}>
<header>{% block header %}Default Header{% endblock %}</header>
<main>{% block content %}{% endblock %}</main>
<footer>{% block footer %}{% endblock %}</footer>
</dialog><twig:Modal>
<twig:block name="header"><h2>Confirm Action</h2></twig:block>
<twig:block name="content"><p>Are you sure?</p></twig:block>
<twig:block name="footer">
<button>Cancel</button>
<button>Confirm</button>
</twig:block>
</twig:Modal>getthis.xxxcomputed#[AsTwigComponent]
final class ProductCard
{
public Product $product;
public function getFormattedPrice(): string
{
return number_format($this->product->getPrice(), 2) . ' EUR';
}
public function isOnSale(): bool
{
return $this->product->getDiscount() > 0;
}
}<div class="product">
<span class="price">{{ this.formattedPrice }}</span>
{% if this.onSale %}
<span class="badge">Sale!</span>
{% endif %}
</div>{{ attributes }}{# Usage #}
<twig:Alert type="info" message="Hello" class="my-class" id="main-alert" data-controller="alert" />
{# In component template -- renders class, id, data-controller #}
<div {{ attributes }}>...</div>{# Merge with defaults #}
<div {{ attributes.defaults({class: 'alert'}) }}>
{# Exclude specific #}
<div {{ attributes.without('id', 'class') }}>
{# Only render specific #}
<div id="{{ attributes.render('id') }}">
{# Check existence #}
{% if attributes.has('disabled') %}#[AsTwigComponent]
final class FeaturedProducts
{
public function __construct(
private readonly ProductRepository $products,
) {}
public function getProducts(): array
{
return $this->products->findFeatured(limit: 6);
}
}{# templates/components/FeaturedProducts.html.twig #}
<div class="featured-products">
{% for product in this.products %}
<twig:ProductCard :product="product" />
{% endfor %}
</div>{# Usage -- no props needed, data comes from service #}
<twig:FeaturedProducts />use Symfony\UX\TwigComponent\Attribute\PreMount;
use Symfony\UX\TwigComponent\Attribute\PostMount;
#[AsTwigComponent]
final class DataTable
{
public array $data;
public string $sortBy = 'id';
#[PreMount]
public function preMount(array $data): array
{
// Modify/validate incoming data before property assignment
$data['sortBy'] ??= 'id';
return $data;
}
#[PostMount]
public function postMount(): void
{
// Runs after all props are set
$this->data = $this->sortData($this->data);
}
}<twig:Card>
<twig:block name="header">
<twig:Icon name="star" /> Featured
</twig:block>
<twig:block name="content">
<twig:ProductList :products="featuredProducts">
<twig:block name="empty">
<twig:Alert type="info" message="No products found" />
</twig:block>
</twig:ProductList>
</twig:block>
</twig:Card># config/packages/twig_component.yaml
twig_component:
anonymous_template_directory: 'components/'
defaults:
App\Twig\Components\: 'components/'{# HTML syntax (recommended -- better IDE support, more readable) #}
<twig:Alert type="success" message="Done!" />
{# Twig syntax (alternative -- useful in edge cases) #}
{% component 'Alert' with {type: 'success', message: 'Done!'} %}
{% endcomponent %}<twig:...>{% component %}ComponentAttributes{{ attributes }}