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 Laravel Package

yiisoft/html

Yii HTML provides tools for dynamic server-side HTML generation in PHP: a dedicated class for each tag, a CustomTag class, widgets like ButtonGroup, CheckboxList and RadioList, automatic encoding with NoEncode, and an Html helper for creating tags and widgets. Requires PHP 8.1–8.5.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer — the package has no service provider, facade, config file, or published assets, so there is nothing to register in config/app.php:

composer require yiisoft/html

Check composer.json first: the constraint is php: 8.1 - 8.5, which is fine for Laravel 10/11/12 apps but rules out any legacy PHP 7.4 project still running Laravel 8.

Where to look first: src/Html.php (the static entry point), src/Tag/ (one class per tag), src/Widget/ (CheckboxList, RadioList, ButtonGroup), and the two docs pages linked from the README: docs/internals.md and docs/testing-id-generation.md. Skim the changelog for 4.0.0 — that is where the behaviour you will find in blog posts diverges from current code.

First use case for a Laravel dev: anywhere you need to produce HTML from plain PHP rather than a Blade template. The canonical example is a queued HTML email rendered from a builder class, a PDF/DOM library that expects markup, a CMS "block" renderer that assembles markup from a JSON config, or a package you maintain that must not depend on the Blade compiler. In a Blade view itself the library is mostly redundant, and the README says as much: for static HTML, write HTML.

A minimal working pass inside an Artisan command or a Mailable:

use Yiisoft\Html\Html;

$html = Html::openTag('table', ['class' => 'table'])
    . Html::tr(
        Html::td('Name') . Html::td('Email'),
        ['class' => 'header']
    )
    . Html::closeTag('table');

Then $this->html($html) (Mailable) or {!! $html !!} in the view — note the raw output tag, because the library already escaped everything.

Implementation Patterns

Compose with tag objects, not string concatenation. Tag objects are immutable and stringable, so they snap together and read like the markup they emit:

use Yiisoft\Html\Html;
use Yiisoft\Html\Tag\Div;
use Yiisoft\Html\Tag\A;

$card = (new Div())
    ->class('card')
    ->id($this->generateCardId($post))
    ->content(
        Html::h2($post->title)->class('card-title'),
        Html::p($post->excerpt)->class('card-text'),
        (new A())->url(route('posts.show', $post))->content('Read more')
    );

Note new Div() rather than Div::tag() — the static constructors were deprecated in 3.13.0 in favour of plain instantiation. Most tutorials and older Yii examples still show ::tag().

Wrap it in Blade components so views stay declarative. The library's value is in PHP-side composition; expose it as a component, not as raw calls sprinkled through templates:

// app/View/Components/Alert.php
public function render(): string
{
    return Html::div($this->message)
        ->class('alert', ['alert-' . $this->type])   // enum values work here too
        ->encode(false)                              // message is pre-built markup
        ->render();
}

Then {!! $component !!} — or better, {{ $component }} if you return an HtmlString. Cast to Illuminate\Support\HtmlString at the boundary so Blade's escaping doesn't fight the library's.

Use the widgets when the data is dynamic. CheckboxList / RadioList remove the loop-and-checked-attribute boilerplate that is genuinely tedious in Blade, including the hidden "uncheck" input that makes unchecked values survive form submission:

use Yiisoft\Html\Widget\CheckboxList\CheckboxList;

$list = (new CheckboxList('roles'))
    ->items($roles)             // [1 => 'Admin', 2 => 'Editor', ...]
    ->value(old('roles', $user->role_ids))   // array-friendly
    ->uncheckValue(0)
    ->containerAttributes(['id' => 'roles']);

echo $list;   // <input type="hidden" name="roles" value="0"> ... name="roles[]"

For per-input decorating, reach for beforeCheckbox() / afterCheckbox() (and the radio equivalents, added in 4.2.0) rather than post-processing the output string.

Build repetitive markup from config, not from templates. Tables, field sets, navigation menus — the pattern is a foreach over a config array producing tag objects, then a single render():

$form = Html::form()
    ->action(route('posts.store'))
    ->method('post')
    ->csrf()                     // or inject the token yourself
    ->content(
        Html::textInput('title', old('title'))->class('form-control'),
        Html::select('status', $statuses, old('status'))->class('form-select'),
        Html::submitButton('Save')->class('btn btn-primary')
    );

Manipulate attributes and classes as data. addClass(), removeClass(), addStyle(), removeStyle(), and the Html::addCssClass() / cssStyleFromArray() / cssStyleToArray() helpers are useful when a base template exists and a caller wants to tweak it — for example, an email layout partial that receives a pre-built button and adds a class. Backed enums are accepted since 3.9.0, which is handy for status-driven classes.

Escape boundaries explicitly. Html::encode() and Html::encodeAttribute() are available when you are assembling attribute values by hand, and NoEncode::string() marks trusted fragments coming out of a sanitizer (e.g. CommonMark output) so nested composition doesn't re-encode them.

Emails and PDFs are the sweet spot. Render the HTML inside a service or a Mailable's build() with the library, hand the string to Mail::html() or to dompdf. You keep the markup in ordinary PHP, testable with assertSeeHtml, and avoid spinning up Blade views for one-off transactional mail.

Gotchas and Tips

Encoding is the thing that will bite you. String content is encoded by default; content that implements NoEncodeStringableInterface (every bundled tag and widget) is not, and its own children are encoded. So Html::b(Html::i('hello')) renders live <i> tags, Html::b('<i>hello</i>') renders &lt;i&gt;, ->encode(false) disables encoding entirely (XSS risk — only for trusted markup), and ->encode(true) forces encoding, which double-escapes nested tag objects. When output looks like visible <div class="card"> on the page, you passed a string where you meant a tag object.

Do not mix escaping models inside Blade. {{ $tag }} and {!! $tag !!} behave differently, and if you already wrap in HtmlString you get raw output once. Pick one convention per component and stick to it. The single most common bug report from Laravel devs adopting this package is a Blade {{ }} around an already-rendered string.

assertSee escapes; assertSeeHtml doesn't. Laravel's assertSee() HTML-escapes the expected value by default. Feature tests that check library output must use assertSeeHtml() (or assertSee($needle, false)), otherwise passing output fails the assertion and the failure message is confusing.

Attribute order is insertion order since 4.0.0. Attribute sorting was removed, so tests and snapshot comparisons should not assume alphabetic ordering. If you upgraded from 3.x and your test suite asserted full tag strings, expect breakage; assert on individual attributes or use assertSeeHtml on substrings.

Tag::id('') throws LogicException. Since 4.0.0 an empty string is rejected rather than treated as "no id". Guard against empty ids coming from DB columns or config before chaining ->id().

Auto-generated IDs make tests flaky. Some helpers call Html::generateId() when no id is supplied, and IDs derive from a counter/state. The package ships test helper functions specifically for controlling this — see docs/testing-id-generation.md. If your assertions include ids, either pin them explicitly with ->id() or use those helpers in setUp().

No facade, no container binding, no static state you can configure. Everything is static methods and objects; that makes it trivially usable in Artisan commands and jobs, but it also means there is no config/html.php to tweak escaping, void element rendering, or attribute formatting (e.g. self-closing <br /> vs <br>). If your front-end tooling cares, you post-process or don't use it for that tag.

Attribute names and values are not validated. Typos like ->atribute('x') are silently rendered as written. Debug by var_dump-ing the properties of the tag object, or call ->render() explicitly — dumping the object does not show the HTML, which trips people up in dd() sessions.

Void tags and content. img, br, input, meta, link etc. are void tags; calling content() on them is not the model. Use the dedicated factories (Html::cssFile(), Html::textInput(), Meta::data()) which encode the correct attribute sets for you — including the loading attribute ordering fix from 3.10.0 for img.

Checkbox/radio naming differs from your expectation. CheckboxList('roles') renders name="roles[]" for the visible inputs and name="roles" for the hidden uncheck input. If you need non-array semantics use Html::getNonArrayableName() / getArrayableName() rather than string surgery. Validation rules on the Laravel side should expect an array for lists.

Integration tips. Keep the dependency at the leaves of your app (services, mailables, view components) and out of controllers. If you need custom escaping or a "trusted HTML" wrapper type of your own, implement NoEncodeStringableInterface on your value object instead of sprinkling encode(false). For custom/unknown tags (<my-widget>, <template> variants), CustomTag plus attribute() is the extension point — subclassing Tag works but the constructor signature and render() contract are the stable surface to lean on, not the protected internals.

Adoption risk, in context. 59 stars and zero dependents outside the Yii ecosystem is a signal about community size, not quality — this is the official Yii 3 HTML library, aggressively static-analysed, with four major versions of changelog history and PHP 8.5 support already landed. The realistic risks are (a) API churn across majors — pin with a caret constraint and read the 4.x changelog before upgrading, and (b) bus-factor support questions, where the Yii forum/Slack is your help channel rather than Laravel's. The BSD-3 licence and the absence of any framework coupling mean it is cheap to remove later: nothing in your app will depend on it beyond the call sites that emit strings.

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.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin