A headless component library for Symfony and Twig, built on Base UI principles. This library provides unstyled, accessible UI components that give you complete control over styling while handling complex functionality, state management, and accessibility out of the box.
Perfect for building custom design systems without fighting against opinionated styles.
- Headless Components: Unstyled components with full control over appearance
- Accessibility First: ARIA attributes and keyboard navigation built-in
- Symfony Integration: Built with Symfony UX Twig Components
- Stimulus Controllers: JavaScript interactivity powered by Stimulus
- Flexible Styling: Use Tailwind, CSS, or any styling solution
- Production Ready: Type-safe, tested, and optimized
- PHP 8.2 or higher
- Symfony 7.4 or higher
- Symfony UX Twig Component ^2.31
- Symfony Stimulus Bundle ^2.0
First, create a bundle directory at the root of your Symfony project and clone the repository:
# Create bundle directory if it doesn't exist
mkdir -p bundle
# Clone the repository into bundle/base-ui
cd bundle
git clone https://github.com/reactic/base-ui.git base-ui
# Or if you already have it, pull latest changes
cd base-ui
git pull origin main
cd ../..For local development (from bundle/ directory), add the repository to your composer.json:
// composer.json
{
"repositories": [
{
"type": "path",
"url": "bundle/base-ui"
}
],
"require": {
"reactic/base-ui": "@dev"
}
}Then install the bundle:
composer require reactic/base-ui:@devAdd the following line to your package.json in the dependencies section:
"@reactic/base-ui": "file:vendor/reactic/base-ui/assets"Complete example:
{
"dependencies": {
"@reactic/base-ui": "file:vendor/reactic/base-ui/assets",
"other-package": "^1.0.0"
}
}Then install the assets:
npm installCompile your assets using your build tool:
# For development with watch mode
npm run watch
# Or for a one-time build
npm run build
# Or for production
npm run build:prodThe bundle should be automatically enabled via Symfony Flex. If not, add it manually:
// config/bundles.php
return [
// ...
Reactic\BaseUi\BaseUiBundle::class => ['all' => true],
];This bundle requires Symfony UX Twig Component and Symfony Stimulus Bundle to work properly.
If you haven't already set them up in your project, please refer to the official documentation:
- Symfony UX Twig Component: https://symfony.com/bundles/ux-twig-component/current/index.html
- Symfony Stimulus Bundle: https://symfony.com/bundles/StimulusBundle/current/index.html
Some components require Stimulus controllers for interactive functionality. You can enable only the controllers you need.
Add the controllers to your assets/controllers.json (or assets/admin/controllers.json for admin-only):
{
"controllers": {
"@reactic/base-ui": {
"accordion": {
"enabled": true,
"fetch": "eager",
"webpackMode": "eager"
},
"link": {
"enabled": true,
"fetch": "eager",
"webpackMode": "eager"
}
}
}
}Available controllers:
accordion- Required for Accordion component interactive behaviorlink- Required for Link component disabled state handling
Note: You can enable only the controllers you need. For example, if you only use the Button component (which doesn't require a controller), you don't need to register any controllers.
If you're using Symfony AssetMapper instead of Webpack Encore, you need to manually configure the Stimulus controllers.
Add the following to your importmap.php:
// importmap.php
return [
// ... other imports
'@reactic/base-ui/accordion' => [
'path' => 'vendor/reactic/base-ui/assets/controllers/accordion_controller.js',
],
'@reactic/base-ui/link' => [
'path' => 'vendor/reactic/base-ui/assets/controllers/link_controller.js',
],
];Import and register the controllers in your main JavaScript file:
// assets/app.js
import { Application } from '@hotwired/stimulus';
// Import Base UI controllers
import AccordionController from '@reactic/base-ui/accordion';
import LinkController from '@reactic/base-ui/link';
// Start Stimulus
const app = Application.start();
// Register Base UI controllers
app.register('reactic--base-ui--accordion', AccordionController);
app.register('reactic--base-ui--link', LinkController);Important: Use the exact controller names (reactic--base-ui--accordion, reactic--base-ui--link) to match the data-controller attributes in the templates.
If you want to use the optional pre-configured styles:
// assets/app.js
import 'vendor/reactic/base-ui/assets/styles/accordion.css';
import 'vendor/reactic/base-ui/assets/components/Link/link.css';Or add them directly in your importmap.php:
'@reactic/base-ui/styles/accordion' => [
'path' => 'vendor/reactic/base-ui/assets/styles/accordion.css',
],
'@reactic/base-ui/components/link' => [
'path' => 'vendor/reactic/base-ui/assets/components/Link/link.css',
],No configuration is required! The bundle works out of the box.
The bundle automatically:
- Registers Twig templates under the
@BaseUinamespace - Configures Twig Components under the
BaseUI:prefix - Loads Stimulus controllers for interactive components
To enable better IDE support and allow template overriding, add this configuration to config/packages/twig_component.yaml:
twig_component:
defaults:
# Map component namespace to template path
Reactic\BaseUi\Component\: '@BaseUi/components/BaseUI/'Benefits:
- ✅ IDE autocomplete for component templates
- ✅ Override templates by creating files in
templates/bundles/BaseUi/components/BaseUI/ - ✅ Better refactoring support in your IDE
Example override:
templates/
└── bundles/
└── BaseUi/
└── components/
└── BaseUI/
└── Button/
└── Button.html.twig # Your custom button template
- Button - Flexible button component with disabled and focus states
- Link - Navigation link with disabled states, external links, and active page indication
- Accordion - Collapsible accordion with keyboard navigation
- Separator - Semantic divider component
A fully accessible button component with flexible rendering options.
Props:
disabled(bool): Disable the button (default:false)focusableWhenDisabled(bool): Keep button focusable when disabled (default:false)tag(string): HTML tag to render (button,a,div, etc.) (default:button)type(string): Button type attribute (default:button)label(string): Button text (default:click me)
Example:
{# Basic button #}
<twig:BaseUI:Button label="Click me" />
{# Disabled button #}
<twig:BaseUI:Button label="Save" disabled />
{# Button with custom content #}
<twig:BaseUI:Button>
<svg>...</svg>
Save Changes
</twig:BaseUI:Button>
{# Render as link #}
<twig:BaseUI:Button tag="a" label="Learn More" />
{# With custom classes #}
<twig:BaseUI:Button
label="Submit"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
/>A collapsible accordion component with full keyboard navigation and ARIA support.
Components:
BaseUI:Accordion- ContainerBaseUI:AccordionItem- Individual accordion itemBaseUI:AccordionHeader- Header wrapperBaseUI:AccordionTrigger- Clickable triggerBaseUI:AccordionPanel- Collapsible content
Example:
<twig:BaseUI:Accordion>
<twig:BaseUI:AccordionItem value="item-1">
<twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionTrigger>
What is Base UI?
</twig:BaseUI:AccordionTrigger>
</twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionPanel>
Base UI is a headless component library that provides
unstyled, accessible components.
</twig:BaseUI:AccordionPanel>
</twig:BaseUI:AccordionItem>
<twig:BaseUI:AccordionItem value="item-2">
<twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionTrigger>
How do I style it?
</twig:BaseUI:AccordionTrigger>
</twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionPanel>
Use CSS classes, Tailwind, or any styling solution you prefer!
</twig:BaseUI:AccordionPanel>
</twig:BaseUI:AccordionItem>
</twig:BaseUI:Accordion>With Tailwind styling:
<twig:BaseUI:Accordion class="space-y-2">
<twig:BaseUI:AccordionItem value="faq-1" class="border rounded-lg">
<twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionTrigger class="w-full px-4 py-3 text-left font-medium hover:bg-gray-50">
Question 1
</twig:BaseUI:AccordionTrigger>
</twig:BaseUI:AccordionHeader>
<twig:BaseUI:AccordionPanel class="px-4 py-3 text-gray-600">
Answer 1
</twig:BaseUI:AccordionPanel>
</twig:BaseUI:AccordionItem>
</twig:BaseUI:Accordion>A fully accessible navigation link component with support for disabled states, external links, and active page indication.
Props:
href(string, required): The destination URLexternal(bool): External link (addstarget="_blank"and security attributes)disabled(bool): Disable the link and prevent navigationactive(bool): Mark as current page (addsaria-current="page")underline(string): Underline control (always,hover,none)target(string): Where to open the linkdownload(string|bool): Force download with optional filenamerel(string): Link relationshiptitle(string): Tooltip text
Example:
{# Basic link #}
<twig:BaseUI:Link href="/about">About Us</twig:BaseUI:Link>
{# External link (opens in new tab with security) #}
<twig:BaseUI:Link href="https://example.com" external="true">
Visit Example
</twig:BaseUI:Link>
{# Disabled link #}
<twig:BaseUI:Link href="/premium" disabled="true" title="Upgrade required">
Premium Feature
</twig:BaseUI:Link>
{# Active link (current page) #}
<twig:BaseUI:Link href="/dashboard" active="true">
Dashboard
</twig:BaseUI:Link>
{# With custom styling #}
<twig:BaseUI:Link
href="/contact"
class="text-blue-600 hover:text-blue-800 underline"
>
Contact
</twig:BaseUI:Link>Requires Stimulus controller - See Setup section above.
A semantic separator/divider component.
Example:
{# Horizontal separator #}
<twig:BaseUI:Separator />
{# With custom styling #}
<twig:BaseUI:Separator class="my-8 border-gray-300" />All components are completely unstyled by default. This gives you full control over the appearance.
The bundle includes optional pre-configured CSS styles for each component. You can import them on-demand in your JavaScript entrypoint:
// Import styles for specific components
import '@reactic/base-ui/styles/accordion.css';
import '@reactic/base-ui/styles/separator.css';
import '@reactic/base-ui/components/Link/link.css';Available style files:
@reactic/base-ui/styles/accordion.css- Basic accordion styling@reactic/base-ui/styles/separator.css- Separator/divider styling@reactic/base-ui/components/Link/link.css- Link component styling (disabled states, external links, etc.)
These styles provide a minimal, functional design that you can use as-is or customize to match your design system.
<twig:BaseUI:Button
label="Click me"
class="btn btn-primary"
/><twig:BaseUI:Button
label="Submit"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
/>Components expose data attributes for styling hooks:
<twig:BaseUI:Button label="Click me" />
{# Renders: <button data-component="button" data-disabled="false">...</button> #}/* Style via data attributes */
[data-component="button"] {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
[data-component="button"][data-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}All components support custom content via blocks:
<twig:BaseUI:Button>
{% block content %}
<svg class="w-4 h-4 mr-2">...</svg>
<span>Save</span>
{% endblock %}
</twig:BaseUI:Button>Use the attributes object to pass any HTML attribute:
<twig:BaseUI:Button
label="Click"
id="my-button"
data-action="click->modal#open"
aria-label="Open modal"
/>bundle/base-ui/
├── assets/
│ ├── controllers/ # Stimulus controllers
│ │ ├── accordion_controller.js
│ │ └── link_controller.js
│ ├── components/Link/ # Link component assets
│ │ └── link.css
│ └── styles/ # Optional CSS examples
│ ├── accordion.css
│ └── separator.css
├── config/
│ └── services.php # Service configuration
├── src/
│ ├── BaseUiBundle.php # Bundle class
│ └── Component/ # PHP component classes
│ ├── Accordion/
│ ├── Button/
│ ├── Link/
│ └── Separator/
├── templates/
│ ├── components/BaseUI/ # Twig templates
│ └── exemple/ # Usage examples
└── composer.json
- Create PHP component class:
// src/Component/MyComponent/MyComponent.php
namespace Reactic\BaseUi\Component\MyComponent;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent(
name: 'BaseUI:MyComponent',
template: '@BaseUi/components/BaseUI/MyComponent/MyComponent.html.twig'
)]
class MyComponent
{
public string $myProp = 'default value';
}- Create Twig template:
{# templates/components/BaseUI/MyComponent/MyComponent.html.twig #}
<div {{ attributes }}>
{{ myProp }}
</div>- Use in your templates:
<twig:BaseUI:MyComponent myProp="Hello!" />All components follow WAI-ARIA best practices:
- Button: Proper ARIA attributes, keyboard support, focus management
- Accordion: ARIA expanded/collapsed states, keyboard navigation (Arrow Up/Down, Home/End)
- Separator: Semantic
<hr>with proper ARIA role
MIT License - see LICENSE file for details
Nicolas Facciolo Email: nicolas@reactic.io
Contributions are welcome! Please feel free to submit a Pull Request.
For issues, questions, or suggestions, please open an issue on the project repository.