-
-
Notifications
You must be signed in to change notification settings - Fork 401
Description
Tip
TL;DR: I'm proposing a new component in Symfony UX to manipulate HTML attributes.
--
I've been working for some time now on a UX HTML component. The starting point is an HTML attributes helper and builder: safe by default, fast, and easy to use and extend. The goal is simple: one solid foundation we can rely on across all Symfony UX packages, and that other packages or CMSes could also use or build on.
Recently, a nice PR with a different approach that was discussed a year ago has resurfaced in the Twig repository, and I think it's worth discussing the options.
The two approaches are almost opposites. One could even argue they are not solving the same problem. But I strongly believe this problem is not how we merge complex structured arrays or attributes.... The problem is that we use complex arrays for something that should be much simpler and more direct. Something that still allows conditionals, callables, and runtime customization, but can also offer more specific implementations (data-, aria-, Stimulus, etc.).
Best way to explain, I'm pasting my README below. Hopefully that's enough to kick off the discussion. I do not have a lot of time these days, so I'd rather not sink time into implementation details if the component itself is not something we want in UX.
Open to any questions, feedback, ideas, or suggestions :)
Why Symfony UX HTML?
The Problem
Building HTML attributes programmatically in PHP is tedious and error-prone:
// Traditional approach
$classes = ['btn', 'btn-primary'];
if ($isDisabled) {
$classes[] = 'disabled';
}
$classAttr = htmlspecialchars(implode(' ', $classes));
$attrs = [];
$attrs[] = 'class="' . $classAttr . '"';
$attrs[] = 'id="my-button"';
if ($isDisabled) {
$attrs[] = 'disabled';
}
$attrs[] = 'aria-label="' . htmlspecialchars($label) . '"';
echo '<button ' . implode(' ', $attrs) . '>Click</button>';The Solution
Symfony UX HTML provides a fluent, type-safe, and secure API:
// in PHP code
$attrs = Attributes::create()
->id('my-button')
->class('btn btn-primary')
->toggle('disabled', $isDisabled)
->ariaLabel($label);
return sprintf('<button %s>Click</button>', $attrs);{# in a Twig template #}
{% set attrs = attributes()
.class('btn btn-primary')
.ariaLabel('Submit form')
.disabled(not post.isEnabled)
%}
<button{{ attrs }}>Submit</button>Key Benefits
| Feature | Traditional | UX HTML |
|---|---|---|
| Safety | Manual htmlspecialchars() |
Auto-escaped ✅ |
| Immutability | Mutable arrays | Immutable objects ✅ |
| Readability | String concatenation | Fluent API ✅ |
| Type Safety | Strings only | Typed methods ✅ |
| Framework Integration | Manual | Twig/Stimulus helpers ✅ |
Perfect For
✅ Symfony/Twig applications
✅ Component libraries (buttons, forms, modals)
✅ Stimulus.js integration
✅ Building accessible HTML (ARIA helpers)
✅ Teams valuing code quality and DX
When NOT to Use
❌ Simple static HTML (overkill)
❌ Performance-critical hot paths with millions of attributes (never)
❌ Non-PHP projects
HTML Attributes is a fluent, immutable API for building and rendering HTML attribute strings in PHP. It was designed with performance, security, and developer experience in mind. Out of the box, it supports:
- Immutable Operations: Every modification returns a new instance.
- Fluent Builder API: Chain methods like
set(),add(),remove(), andtoggle(). - Magic Methods: Enable natural method calls (e.g.
->ariaLabel('Close')or->disabled()). - Namespaced Helpers: Dedicated helpers for ARIA, Stimulus, and generic data attributes.
- Secure Rendering: All output is properly escaped.
- High Performance: Optimized for the most common attribute operations.
Installation
composer require symfony/ux-html-attributesBasic Usage
The library provides a single entry point to build an attribute collection and render it as a string:
Core API
use Symfony\UX\Html\Attribute\Attributes;
$attributes = Attributes::create()
->set('id', 'my-id')
->add('class', 'btn')
->add('class', 'btn-primary')
->toggle('disabled', true)
->remove('hidden');DX-oriented API
$attr = Attributes::create()
->enableMagicMethods()
->href('/smnandre') // Named methods
->class('btn btn-sm btn-red') // Set multiple classes
->rel('external me') // Join multiple values
->disabled(true) // Boolean attributes
->ariaLabel('Hello') // Aria namespaced helpers
->title('Ah < Bh'); // String escaping
echo $attr->render(); // Safe HTML rendering<output
href="/smnandre" class="btn btn-sm btn-red"
rel="external me" disabled aria-label="Hello" title="Ah < Bh" />Twig Extension
Use the Twig helper to fluently compose attributes inside your templates:
{# templates/components/button.html.twig #}
{% set attrs = attributes()
.class('btn btn-primary')
.ariaLabel('Submit form')
.disabled(not isEnabled)
%}
<button{{ attrs }}>Submit</button>Features
Core API
Immutable
Each method returns a new instance.
Basic Methods:
- set(string $name, string|bool|null $value): self
Create or replace an attribute. Usetruefor boolean attributes andfalseornullto remove them. Join arrays with spaces before passing. - add(string $name, string|bool|null $value): self
Append to an existing attribute. When both values are strings they are concatenated with a space. - remove(string $name): self
Remove an attribute from the collection. - toggle(string $name, bool $condition): self
Add the attribute when$conditionistrue, otherwise remove it. - get(string $name): string|bool|null
Fetch the raw value of an attribute. - all(): array
Return all attributes as an associative array. - render(): string
Render the attribute string with proper escaping.
Magic Methods:
Calls such as ->disabled(), ->ariaLabel('Close'), or ->foo('bar') are automatically converted to attribute names
in kebab-case and handled by the core API.
Namespaced Helpers
ARIA Attributes
Use the dedicated helper to set ARIA attributes:
$attributes->aria()->set('label', 'Close');
// Sets "aria-label" to "Close"Data Attributes
Use the generic data helper to manage custom data-* attributes:
$attributes->data()->set('foo', 'bar');
// Sets "data-foo" to "bar"Stimulus Attributes
Use the Stimulus helper to manage data-* attributes and controllers:
$attributes = Attributes::create()
->stimulus()->setController('dropdown')
->stimulus()->addController('modal')
->stimulus()->set('action', 'click->example#toggle');
echo $attributes->render();
// data-controller="dropdown modal" data-action="click->example#toggle"Advanced Usage
Boolean and Array Values
Boolean attributes are enabled by passing true and removed when false or
null is used:
$attributes->disabled(); // Adds "disabled"
$attributes->hidden(false); // Removes "hidden"
$attributes->hidden(true); // Adds "hidden"When working with arrays of values (e.g. classes) join them with spaces before
calling set() or add():
$classes = ['btn', 'btn-primary'];
$attributes->set('class', implode(' ', $classes));echo $attributes->render();Magic Methods
$attributes = Attributes::create()->enableMagicMethods();
$attributes->ariaLabel('Accessible Label'); // Sets aria-label="Accessible Label"
$attributes->foo('bar'); // Sets foo="bar"
> [!IMPORTANT]
> The magic methods are NOT enabled by default. Call `enableMagicMethods()` on the factory or an instance to enable them.
### Combining Attributes
```php
$attributes = Attributes::create()
->set('id', 'example')
->aria()->set('expanded', true)
->stimulus()->addController('dropdown');
echo $attributes->render();
// Output might be: id="example" aria-expanded="true" data-controller="dropdown"
Stimulus Helpers
$attributes->stimulus()->setController('modal');
$attributes->stimulus()->addController('dropdown');
$attributes->stimulus()->set('action', 'click->dropdown#toggle');Future Features
Additional merging strategies and extensions are already planned for future releases (tailwind merge, CSS scoping, references...)
[...]