lemmon / twig-jsx
JSX-like component syntax for Twig
v0.1.0
2026-05-15 08:19 UTC
Requires
- php: ^8.1
- twig/twig: ^3.0
Requires (Dev)
- carthage-software/mago: ~1.27.1
- ergebnis/composer-normalize: ^2.51
- phpunit/phpunit: ^10.5
README
JSX-like component syntax for Twig.
Write <Alert {type} important /> instead of a verbose {% include %} call. Twig JSX transforms JSX-like component tags into native Twig {% include %} and {% embed %} calls at the lexer level — no runtime overhead, no Symfony dependency.
Requirements
- PHP
^8.1 - Twig
^3.0
Installation
composer require lemmon/twig-jsx
Quick Start
1. Register the Extension and Lexer
use Lemmon\TwigJsx\JSXPreLexer; use Lemmon\TwigJsx\AttributeExtension; $twig = new \Twig\Environment($loader); $twig->addExtension(new AttributeExtension()); $twig->setLexer(new JSXPreLexer($twig));
2. Create a Component
Save your component in templates/components/Alert.twig:
{% set type = props.type|default('info') %}
{% set important = props.important|default(false) %}
{% set title = props.title|default(null) %}
{% set message = props.message|default('No message provided.') %}
<div class="alert alert-{{ type }}{% if important %} alert-important{% endif %}{% if props.class|default('') %} {{ props.class }}{% endif %}"
{{ props.except('type', 'important', 'title', 'message', 'class')|render }}>
{% if title %}<strong>{{ title }}</strong>{% endif %}
{% block content %}{{ message }}{% endblock %}
</div>
3. Use It in a Template
<!-- Self-closing with shorthand --> <Alert {type} important message="Everything is great!" /> <!-- With children and extra attributes --> <Alert type="warning" class="shadow-lg" data-id="123"> <strong>Wait!</strong> Something needs your attention. </Alert>
Prop Syntax
| Syntax | Example | Compiles to (inside props bag) |
|---|---|---|
| Static | type="info" |
'type': 'info' |
| Expression | type={userType} |
'type': userType |
| Expression | count={items|length} |
'count': items|length |
| Expression | theme={dark ? 'd' : 'l'} |
'theme': dark ? 'd' : 'l' |
| Shorthand | {type} |
'type': type |
| Boolean | important |
'important': true |
How a Component Reads Its Inputs
Every prop the caller passes — semantic inputs and HTML attributes alike — arrives in a single
props bag of type ComponentAttributes. The component template decides what to extract:
{# Destructure semantic inputs as locals #} {% set type = props.type|default('info') %} {% set message = props.message|default('') %} {# Spread the remaining keys as HTML attributes #} <div class="alert-{{ type }}" {{ props.except('type', 'message')|render }}> {{ message }} </div>
props.key— read any valueprops.except('a', 'b', ...)— returns a new bag without the listed keys; useful for spreading HTML fallthrough attributes onto the root element{{ props|render }}— renders all entries as HTML attribute pairs
Configuration
| Option | Default | Description |
|---|---|---|
directory |
components |
Subdirectory inside templates/ where component files are looked up. |
extension |
.twig |
File extension for component templates. |
prefix |
"" |
Tag prefix. When empty, any Capitalized tag is treated as a component (JSX-style). |
props_variable |
props |
Name of the variable that holds all props in the component template. |
content_block |
content |
Twig block name where a bodied tag's children are rendered. |
Alternatives
- Symfony UX Twig Component — PHP class-backed components; requires the Symfony UX bundle.
- TwigX — similar
<Component />syntax as a Symfony bundle; requires Symfony Config and DI.
Contributing
Bug reports and pull requests are welcome on GitHub.
License
MIT. See LICENSE.