$npx -y skills add smnandre/symfony-ux-skills --skill twig-componentSymfony 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 (template-
| 1 | # TwigComponent |
| 2 | |
| 3 | Reusable UI components with PHP classes + Twig templates. Think React/Vue components, but server-rendered with zero JavaScript. |
| 4 | |
| 5 | Two flavors exist: **class components** (PHP class + Twig template) for components that need logic, services, or computed properties, and **anonymous components** (Twig-only, no PHP class) for simple presentational elements. |
| 6 | |
| 7 | ## When to Use TwigComponent |
| 8 | |
| 9 | Use TwigComponent when you need reusable markup with props but no server re-rendering after the initial render. If the component needs to react to user input (re-render via AJAX, data binding, actions), use LiveComponent instead. |
| 10 | |
| 11 | Good candidates: buttons, alerts, cards, badges, icons, form widgets, layout sections, navigation items, table rows, modals (structure only). |
| 12 | |
| 13 | ## Installation |
| 14 | |
| 15 | ```bash |
| 16 | composer require symfony/ux-twig-component |
| 17 | ``` |
| 18 | |
| 19 | ## Class Component |
| 20 | |
| 21 | A PHP class annotated with `#[AsTwigComponent]` paired with a Twig template. |
| 22 | |
| 23 | ```php |
| 24 | // src/Twig/Components/Alert.php |
| 25 | namespace App\Twig\Components; |
| 26 | |
| 27 | use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; |
| 28 | |
| 29 | #[AsTwigComponent] |
| 30 | final class Alert |
| 31 | { |
| 32 | public string $type = 'info'; |
| 33 | public string $message; |
| 34 | public bool $dismissible = false; |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | ```twig |
| 39 | {# templates/components/Alert.html.twig #} |
| 40 | <div class="alert alert-{{ type }}" {{ attributes }}> |
| 41 | {{ message }} |
| 42 | {% if dismissible %} |
| 43 | <button type="button" class="close">×</button> |
| 44 | {% endif %} |
| 45 | </div> |
| 46 | ``` |
| 47 | |
| 48 | ```twig |
| 49 | {# Usage #} |
| 50 | <twig:Alert type="success" message="Saved!" /> |
| 51 | <twig:Alert type="danger" message="Error occurred" dismissible /> |
| 52 | |
| 53 | {# With block content instead of message prop #} |
| 54 | <twig:Alert type="warning"> |
| 55 | <strong>Warning:</strong> Check your input |
| 56 | </twig:Alert> |
| 57 | ``` |
| 58 | |
| 59 | ## Anonymous Component (Twig Only) |
| 60 | |
| 61 | No PHP class needed. Props are declared with `{% props %}` directly in the template. Use for simple presentational components with no logic. |
| 62 | |
| 63 | ```twig |
| 64 | {# templates/components/Button.html.twig #} |
| 65 | {% props variant = 'primary', size = 'md', disabled = false %} |
| 66 | |
| 67 | <button |
| 68 | class="btn btn-{{ variant }} btn-{{ size }}" |
| 69 | {{ disabled ? 'disabled' }} |
| 70 | {{ attributes }} |
| 71 | > |
| 72 | {% block content %}{% endblock %} |
| 73 | </button> |
| 74 | ``` |
| 75 | |
| 76 | ```twig |
| 77 | <twig:Button variant="danger" size="lg">Delete</twig:Button> |
| 78 | ``` |
| 79 | |
| 80 | ## Props |
| 81 | |
| 82 | ### Public Properties (Class Components) |
| 83 | |
| 84 | Public properties become props. Required props have no default value. |
| 85 | |
| 86 | ```php |
| 87 | #[AsTwigComponent] |
| 88 | final class Card |
| 89 | { |
| 90 | public string $title; // Required |
| 91 | public ?string $subtitle = null; // Optional |
| 92 | public bool $shadow = true; // Optional with default |
| 93 | } |
| 94 | ``` |
| 95 | |
| 96 | ### mount() for Derived State |
| 97 | |
| 98 | Use `mount()` to compute values from incoming props. The method runs once during component initialization. |
| 99 | |
| 100 | ```php |
| 101 | #[AsTwigComponent] |
| 102 | final class UserCard |
| 103 | { |
| 104 | public User $user; |
| 105 | public string $displayName; |
| 106 | |
| 107 | public function mount(User $user): void |
| 108 | { |
| 109 | $this->user = $user; |
| 110 | $this->displayName = $user->getFullName(); |
| 111 | } |
| 112 | } |
| 113 | ``` |
| 114 | |
| 115 | ```twig |
| 116 | <twig:UserCard :user="currentUser" /> |
| 117 | ``` |
| 118 | |
| 119 | ### Dynamic Props (Colon Prefix) |
| 120 | |
| 121 | Prefix a prop with `:` to pass a Twig expression instead of a string literal. |
| 122 | |
| 123 | ```twig |
| 124 | {# Pass a variable #} |
| 125 | <twig:Alert :type="alertType" :message="flashMessage" /> |
| 126 | |
| 127 | {# Pass an expression #} |
| 128 | <twig:UserList :users="users|filter(u => u.active)" /> |
| 129 | ``` |
| 130 | |
| 131 | ## Blocks (Slots) |
| 132 | |
| 133 | Blocks let parent templates inject content into specific areas of a component. |
| 134 | |
| 135 | ### Default Block |
| 136 | |
| 137 | Content between component tags goes to `{% block content %}`: |
| 138 | |
| 139 | ```twig |
| 140 | {# Component template #} |
| 141 | <div class="card">{% block content %}{% endblock %}</div> |
| 142 | |
| 143 | {# Usage #} |
| 144 | <twig:Card><p>This is the card content</p></twig:Card> |
| 145 | ``` |
| 146 | |
| 147 | ### Named Blocks |
| 148 | |
| 149 | ```twig |
| 150 | {# templates/components/Modal.html.twig #} |
| 151 | <dialog class="modal" {{ attributes }}> |
| 152 | <header>{% block header %}Default Header{% endblock %}</header> |
| 153 | <main>{% block |