reactolith / symfony-bundle
Symfony integration for Reactolith – Twig form theme, attribute helper, and HTTP/2 component preloading.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
pkg:composer/reactolith/symfony-bundle
Requires
- php: >=8.2
- symfony/event-dispatcher: ^6.4 || ^7.0 || ^8.0
- symfony/form: ^6.4 || ^7.0 || ^8.0
- symfony/framework-bundle: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/twig-bundle: ^6.4 || ^7.0 || ^8.0
- twig/twig: ^3.0 || ^4.0
Requires (Dev)
- phpunit/phpunit: ^10.0 || ^11.0
- symfony/phpunit-bridge: ^6.4 || ^7.0 || ^8.0
- symfony/translation: ^6.4 || ^7.0 || ^8.0
Suggests
- pentatrion/vite-bundle: Recommended for Vite asset integration (vite_entry_script_tags / vite_entry_link_tags)
README
Symfony integration for Reactolith -- server-side HTML hydration with React components.
Write your UI in Twig. Get React-powered interactive components. No JavaScript in your Symfony code.
Reactolith lets you render HTML with custom tags like <ui-button>, <ui-input>, <ui-select> from your Symfony backend. On the client side, Reactolith automatically hydrates these tags into fully interactive React components.
This bundle provides the Symfony-side integration:
re_attrsFilter -- Renders prop objects as correct HTML attributes (string, boolean, JSON)- HTTP/2 Preload -- Optional event listener that sends
X-Reactolith-Componentsheader with all tags on the page - Custom FormTypes --
SwitchTypeand more - Form Theme -- Optional Twig form theme for reactolith/ui (shadcn-based components)
Requirements
- PHP >= 8.2
- Symfony 6.4, 7.x, or 8.x
Installation
composer require reactolith/symfony-bundle
The bundle registers itself automatically via Symfony Flex. If you're not using Flex, add it manually to config/bundles.php:
Reactolith\SymfonyBundle\ReactolithBundle::class => ['all' => true],
Configuration
# config/packages/reactolith.yaml reactolith: tag_prefix: 'ui-' # HTML tag prefix, must match /^[a-z][a-z0-9]*-$/ preload: enabled: false # opt-in: sends X-Reactolith-Components response header
| Option | Default | Description |
|---|---|---|
tag_prefix |
ui- |
HTML tag prefix for components. Must be lowercase, ending with -. |
preload |
false |
Enable the HTTP/2 component preload listener. |
re_attrs Filter / Function
Renders an associative array as HTML attributes. Available as both a Twig filter and function.
{# As a filter #} <ui-toaster {{ {position: 'top-right', 'rich-colors': true, toasts: toasts}|re_attrs }} /> {# As a function #} <ui-dialog {{ re_attrs({open: isOpen, config: {animate: true}}) }}>
Rendering Rules
| Value Type | Output | Example |
|---|---|---|
| String | name="value" |
variant="outline" |
| Number | name="42" |
count="42" |
true |
name |
disabled |
false / null |
(omitted) | |
| Array / Object | json-name='...' |
json-config='{"a":1}' |
This replaces verbose manual encoding like json-toasts="{{ toasts|json_encode }}" with {{ {toasts: toasts}|re_attrs }}.
HTTP/2 Component Preloading
When enabled, the listener scans each HTML response for <ui-*> tags and adds a response header:
reactolith: preload: enabled: true
X-Reactolith-Components: ui-button, ui-input, ui-label, ui-toaster
A reverse proxy or CDN can use this header to push the corresponding JavaScript chunks via HTTP/2.
Custom Tag Prefix
reactolith: tag_prefix: 'x-'
<x-input type="text" ... /> <x-button type="submit">Submit</x-button>
The prefix is available in Twig as the global {{ reactolith_tag_prefix }}.
Using with reactolith/ui (shadcn)
reactolith/ui is a shadcn-based component library that works seamlessly with this bundle. The following steps set up a complete Symfony + Vite + React + shadcn/ui stack.
1. Install frontend dependencies
npm install reactolith react react-dom @loadable/component npm install -D vite vite-plugin-symfony @vitejs/plugin-react @tailwindcss/vite tailwindcss
For shadcn/ui components:
npm install @base-ui/react class-variance-authority clsx tailwind-merge lucide-react npm install -D @types/react @types/loadable__component
Optional (for enhanced styling):
npm install @fontsource-variable/inter tw-animate-css
2. Install pentatrion/vite-bundle
composer require pentatrion/vite-bundle
3. Initialize shadcn/ui with reactolith/ui registry
npx shadcn@latest init
When prompted, configure:
- TypeScript: Yes
- Style: base-nova (or your preference)
- Base color: neutral (or your preference)
- CSS variables: Yes
- Import alias: @/*
- React Server Components: No
After initialization, update components.json to include the reactolith/ui registry:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "assets/app.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@reactolith": "https://reactolith.github.io/ui/r/{name}.json"
}
}
4. Add reactolith/ui components
npx shadcn@latest add @reactolith/button @reactolith/input @reactolith/label @reactolith/textarea
Components will be created in assets/components/app/.
5. Vite configuration
// vite.config.js import path from "path" import symfonyPlugin from "vite-plugin-symfony"; import tailwindcss from "@tailwindcss/vite" import react from "@vitejs/plugin-react" import { defineConfig } from "vite" export default defineConfig({ plugins: [ react(), tailwindcss(), symfonyPlugin({ refresh: true }), ], resolve: { alias: { "@": path.resolve(__dirname, "./assets"), }, }, build: { rollupOptions: { input: { app: "./assets/app.ts", }, }, }, });
6. TypeScript configuration
Create tsconfig.json:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./assets/*"] }
}
}
Create tsconfig.app.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"baseUrl": ".",
"paths": { "@/*": ["./assets/*"] }
},
"include": ["assets"]
}
Create tsconfig.node.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true
},
"include": ["vite.config.js"]
}
7. CSS entry point
/* assets/app.css */ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; @import "@fontsource-variable/inter"; @custom-variant dark (&:is(.dark *)); /* Theme variables are auto-generated by shadcn init */
8. JavaScript entry point
Create assets/app.ts:
import "./app.css"; import loadable from "@loadable/component"; import { App } from "reactolith"; import type { ComponentType } from "react"; const modules = import.meta.glob<{ default: ComponentType<any> }>("@/components/app/**/*.tsx"); new App( loadable(({ is }: { is: string }) => { const name = is.substring(3) const match = Object.keys(modules).find(key => key.endsWith(`/${name}.tsx`)); if (!match) throw new Error(`Component not found: ${is}`); return modules[match](); }, { cacheKey: ({ is }: { is: string }) => is, }) as unknown as ComponentType<Record<string, unknown>>, );
This uses Vite's import.meta.glob to dynamically load components from assets/components/app/. Each component maps to <ui-{name}> by stripping the ui- prefix.
9. Base template
{# templates/base.html.twig #} <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %} {{ vite_entry_link_tags('app') }} {% endblock %} {% block javascripts %} {{ vite_entry_script_tags('app', { dependency: 'react' }) }} {% endblock %} </head> <body> <div id="reactolith-app"> {% block body %}{% endblock %} </div> </body> </html>
The <div id="reactolith-app"> wrapper is required -- Reactolith uses it as the root element for React hydration.
10. Run the dev server
npx vite # Start Vite dev server symfony serve # Start Symfony dev server
Form Theme (reactolith/ui)
This bundle ships a Twig form theme that maps standard Symfony form types to <ui-*> tags. It is designed for use with reactolith/ui components.
To activate it, add it to your Twig configuration:
# config/packages/twig.yaml twig: form_themes: - '@Reactolith/form/reactolith_layout.html.twig'
Or apply it per form in a template:
{% form_theme form '@Reactolith/form/reactolith_layout.html.twig' %}
Supported Mappings
| Symfony Type | Reactolith Tag |
|---|---|
TextType |
<ui-input type="text"> |
EmailType |
<ui-input type="email"> |
PasswordType |
<ui-input type="password"> |
NumberType |
<ui-input type="number"> |
UrlType |
<ui-input type="url"> |
SearchType |
<ui-input type="search"> |
TelType |
<ui-input type="tel"> |
TextareaType |
<ui-textarea> |
CheckboxType |
<ui-checkbox> |
SwitchType (custom) |
<ui-switch> |
ChoiceType (select) |
<ui-select> |
ChoiceType (radio) |
<ui-radio-group> |
SubmitType |
<ui-button type="submit"> |
ButtonType |
<ui-button> |
Unsupported types fall back to Symfony's form_div_layout.html.twig.
Form Row Structure
<div class="space-y-2"> <ui-label for="...">Label</ui-label> <!-- widget --> <p class="text-sm text-muted-foreground">Help text</p> <p class="text-sm font-medium text-destructive">Error message</p> </div>
SwitchType
use Reactolith\SymfonyBundle\Form\Type\SwitchType; $builder->add('darkMode', SwitchType::class, ['label' => 'Enable dark mode']);
<ui-switch id="form_darkMode" name="form[darkMode]" json-checked="false" />
Extending the Form Theme
{# templates/form/my_theme.html.twig #} {% use "@Reactolith/form/reactolith_layout.html.twig" %} {% block form_row %} <div class="my-custom-wrapper"> {{ form_label(form) }} {{ form_widget(form) }} {{ form_errors(form) }} </div> {% endblock %}