Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Html Extra Laravel Package

twig/html-extra

Twig extension adding data_uri, html_classes, and html_cva filters/functions for generating RFC 2397 data URLs, conditionally joining class names, and managing class variants with Cva objects.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. The package is a plain Twig extension — one class, no service provider, no config file of its own.

composer require twig/html-extra:^3.29

Register it on the Twig environment your app actually renders with:

use Twig\Extra\Html\HtmlExtension;

// Standalone Twig
$twig = new \Twig\Environment($loader);
$twig->addExtension(new HtmlExtension());

// Laravel + a Twig bridge (e.g. twigbridge / twig/laravel):
// add to config/twigbridge.php → 'extensions' => [HtmlExtension::class]

Where to look first. Four doc pages, all short: data_uri (filter), html_classes (function), html_cva (function), html_attr (function). html_attr arrived in 3.24 and html_attr_value in 3.29 — neither appears in older blog posts, so check composer show twig/html-extra before copying snippets.

First use case — conditional classes. The thing you will reach for daily is html_classes, replacing the class="btn {{ $active ? 'is-active' : '' }}" mess:

<a class="{{ html_classes('nav-link', {'is-active': isCurrent, 'is-disabled': not enabled}) }}">
  {{ label }}
</a>

That is the whole 80% use case. Get this working in one partial before touching anything else.

Implementation Patterns

html_classes is variadic, not a filter. You can mix strings, nested arrays, and condition maps in one call; falsy conditions are dropped and null/false entries vanish. It also accepts objects implementing __toString, which is handy when a PHP view model exposes a badge() value.

{{ html_classes('card', sizeClass, ['shadow', 'rounded'], {'card--muted': isMuted}) }}

Wrap it in a macro, not in every template. The real win is one place that knows your class conventions:

{# partials/_classes.twig #}
{% macro button(variant, size, extra = {}) %}
  {{ html_classes('btn', 'btn--' ~ variant, 'btn--' ~ size, extra) }}
{% endmacro %}

html_cva for variant sets. Define the variant map once (a _components.twig imported with {% import %}), then apply per call site:

{% set button = html_cva('btn', {
  color: {primary: 'btn--primary', danger: 'btn--danger'},
  size:  {sm: 'btn--sm', lg: 'btn--lg'}
}, {}, {color: 'primary', size: 'sm'}) %}

<button class="{{ button.apply({color: 'danger'}) }}">Delete</button>

Treat the Cva object as a component definition: define once, apply() with the runtime state, and stringify at the point of output. Because it is immutable, apply() returns a new object — never expect apply() to mutate the set variable.

html_attr for attribute bags. The one that removes the most template noise. It takes a map and returns escaped, ready-to-print attributes, skipping null/false values:

<div {{ html_attr({
  'id': id,
  'class': html_classes('panel', {'panel--open': open}),
  'data-controller': 'panel',
  'aria-expanded': open ? 'true' : 'false',
  'style': 'opacity: ' ~ opacity,
  'hidden': not open
}) }}>

Note the space before {{ — the function does not emit leading whitespace.

html_attr_value for hand-rolled attributes (3.29+). The per-value escaping inside html_attr is now exposed on its own, for the cases the map form cannot cover: attribute names computed at runtime, attributes split across conditional branches, or a loop over a data-* array.

{% for name, value in dataAttributes %}
  {{ name }}="{{ html_attr_value(value) }}"
{% endfor %}

You get the same escaping and null/false handling as html_attr without re-implementing |e guards next to every dynamic name. Prefer html_attr whenever the whole map is known up front — it also handles the skipping — and reach for html_attr_value only when the name is not.

data_uri for inline assets. Good for small icons, fallback images, and email templates where external requests are blocked:

<img src="{{ asset_path|data_uri }}" alt="">

The filter accepts either a filesystem path (it reads the file and guesses the MIME type) or a raw string, with an optional second argument to force the type: {{ svg_source|data_uri('image/svg+xml') }}. In Laravel, resolve the path in PHP (public_path(...)) and pass it in — do not rely on relative paths from templates.

Recommended split of responsibility. Do the heavy computation in PHP (a view composer or a small ClassBuilder), keep only boolean toggles and variant selection in Twig. html_cva is a good home for design-system variants; it is a bad home for business rules.

Gotchas and Tips

  • 'btn'|html_classes(...) will not work. It is a function, not a filter. A lot of copied snippets online get this wrong, and the error (Unknown "html_classes" filter) sends people looking in the wrong place. Same for html_cva, html_attr, and html_attr_value.
  • Escaping. html_classes returns a plain string and is escaped normally — fine inside class="...". html_attr and html_attr_value are marked safe because they escape their inputs themselves; never wrap any of them in |raw to "fix" output, that is where XSS creeps in.
  • Version floors matter. html_attr needs 3.24+, html_attr_value needs 3.29+, and the zero-value html_attr fix below landed in 3.29. Pin "twig/html-extra": "^3.29" and stop chasing Unknown "html_attr" function compile errors caused by a stale lockfile.
  • Zero values and html_attr (fixed in 3.29). Earlier 3.x builds dropped style declarations whose value was zero, so 'style': 'opacity: ' ~ 0 or similar could silently disappear from the markup — a nasty, invisible-in-the-diff regression for transitions and animation states. If you render zero-valued styles or counters, upgrade and add a rendered-output test that asserts opacity: 0 survives.
  • html_attr_value escapes a value, it does not build an attribute. It emits no name="..." and does not skip the attribute for you on every path — if the value is null/false you still decide whether the attribute should be printed at all. Use html_attr when you have the full map, and never feed an attribute name through it expecting sanitisation.
  • Double registration. Adding HtmlExtension both in config/twigbridge.php and a boot() method throws "extension already registered". Pick one. After changing registration in Laravel, php artisan config:clear and clear the compiled Twig cache directory — Twig caches compiled templates and will happily serve the old extension set (which looks exactly like a missing html_attr_value).
  • Email / secondary environments. If you spin up a separate Twig environment for mail or PDF rendering, it needs its own addExtension(new HtmlExtension()). "Works in views, fails in emails" is almost always this.
  • data_uri has no size guard. It will base64 a 2 MB hero image into your HTML, inflating it ~33%, killing caching, and blowing past any Content-Security-Policy that does not allow data:. Enforce a size threshold in PHP before calling it; a <10KB rule of thumb is common.
  • data_uri and untrusted input. data:text/html and similar payloads in src/href are an XSS vector in some browsers. Only feed it paths or strings you control.
  • MIME guessing fails on extensionless assets. SVG sprites and generated files often need the explicit second argument.
  • Keep the trade-off honest in a Laravel codebase. If you are on Blade, @class([...]) and $attributes->merge([...]) already cover html_classes and most of html_attr; data_uri is a three-line helper. Adopting this package only pays off if Twig is already your rendering engine — do not introduce a Twig bridge solely for these three functions.
  • Testing without a container. Instantiate new \Twig\Environment(new \Twig\Loader\ArrayLoader([...])), add the extension, and assert on the rendered markup. The extension is isolated enough that unit tests need no Laravel bootstrapping — which also makes it a safe place to add regression tests around your class conventions and the zero-value rendering above.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode