arcanedev/laravel-html
Arcanedev/Laravel HTML provides HTML and form builders for Laravel, letting you generate forms, inputs, links, and other markup with a fluent PHP API. It simplifies Blade templates, reduces repetitive HTML, and supports Laravel conventions.
arcanedev/laravel-html restores the classic Laravel 4 Html / Form facades — fluent builders that emit escaped-ready markup from PHP instead of hand-written Blade. It is a maintained-lineage fork of laravelcollective/html with the same API surface, so anything you remember from that package transfers directly.
Install and publish
composer require arcanedev/laravel-html
php artisan vendor:publish --provider="Arcanedev\LaravelHtml\LaravelHtmlServiceProvider"
The service provider is auto-discovered on Laravel 5.5+, and it registers the Html and Form aliases for you — do not add them to config/app.php yourself. The published config/laravel-html.php controls the framework flavour used for component classes and which icon set the helpers assume; for a plain Tailwind/Bootstrap app set it once and forget it.
Where to look first
Arcanedev\LaravelHtml\HtmlBuilder — script, style, image, link, linkRoute, mailto, ul/ol, attributes, tag, entities/decode.Arcanedev\LaravelHtml\FormBuilder — open/close, model, and every field type plus label, submit, token, selectRange/selectYear/selectMonth.Contracts directory — bind these if you want constructor injection inside your own services or test doubles.First use case — a create/edit form that does the boring parts for you
{{-- resources/views/posts/edit.blade.php --}}
{!! Form::model($post, ['route' => ['posts.update', $post], 'method' => 'PATCH', 'files' => true]) !!}
<div class="field">
{!! Form::label('title', 'Title') !!}
{!! Form::text('title', null, ['class' => 'input', 'required' => true]) !!}
@error('title') <p class="error">{{ $message }}</p> @enderror
</div>
<div class="field">
{!! Form::label('body', 'Body') !!}
{!! Form::textarea('body', null, ['rows' => 6, 'class' => 'input']) !!}
</div>
{!! Form::select('status', ['draft' => 'Draft', 'published' => 'Published'], null, ['class' => 'input']) !!}
{!! Form::submit('Save', ['class' => 'btn']) !!}
{!! Form::close() !!}
That single call gives you: the _token hidden field, the _method=PATCH spoof, enctype="multipart/form-data", prefilled values from $post, and — after a failed validation redirect — the flashed old input overriding the model values. That old-input-first resolution order is the entire value proposition of the package.
Model binding as the default for edit screens. Form::model($entity, $options) sets an internal model; every subsequent field resolves its value as old input → model attribute → explicit argument. Always pair it with Form::close() — that is what emits the _method override, not Form::open(). For the create screen, use Form::open(['route' => 'posts.store']) and pass explicit values only where you need a default.
Route/action targeting. Prefer 'route' => ['posts.update', $post] over url() strings or action() arrays; it keeps the form correct under locale prefixes, subdomains, and signed routes. Form::open(['route' => 'x', 'secure' => true]) forces HTTPS for the action URL.
Macros: turn the builder into your design system. This is the pattern that keeps the package from becoming a maintenance burden.
// AppServiceProvider::boot()
Form::macro('textField', function ($name, $label, $value = null, array $attrs = []) {
return new HtmlString(
Form::label($name, $label)
. Form::text($name, $value, array_merge(['class' => 'input'], $attrs))
);
});
Html::macro() works the same way for non-form components. Macros are per-process, so re-register inside tests (setUp) or use the container binding instead if you need them in queue workers.
Array-notation fields. Names like items[0][qty] and tags[] are supported, and FormBuilder::transformKey() rewrites ., [ and ] to _ for the id/for pair — so Form::label('items[0][qty]') and Form::text('items[0][qty]') stay correctly associated. Build repeated rows with a @foreach over old input (old('items', $defaults)) so a failed save doesn't lose rows.
Lists and assets. HTML::ul($collection, ['class' => 'list']) renders nested arrays recursively; HTML::script('js/app.js', ['defer']), HTML::style(...), and HTML::image('img/logo.png', 'Logo', ['class' => 'h-8']) keep asset markup consistent. HTML::attributes($array) is a useful standalone serialiser for your own custom components — it flattens array values with spaces and drops null/false booleans.
Composition with validation and Blade. The builder is orthogonal to @error / $errors; render messages yourself next to each field. For value display (non-input) use {{ $post->body }}; the Form builders are only for controls. If you build a SPA/Inertia front end, the package becomes largely dead weight — you'll only use Html::attributes().
Version compatibility is the real risk. Last tagged release is Nov 2020, targeting Laravel 8-era APIs. There is no Laravel 9/10/11 support, and Laravel's Str/HtmlString refactors plus the removal of legacy container aliasing break it. Pin with an explicit constraint, and treat adoption as a short-to-medium-term decision: plan a migration to Blade components (<x-form.input>) or spatie/laravel-html before your next major Laravel upgrade. With 6 stars and 0 dependents, expect no upstream fixes.
Alias collisions. Html and Form are global aliases. If laravelcollective/html or any package registering the same names is present, resolution becomes order-dependent and you'll get "method not found" at random. Check php artisan about / config/app.php for duplicates.
Escaping and HtmlString. Field values are HTML-escaped by the builder; that's correct behaviour. If you need pre-rendered markup inside a label, wrap it in HTML::decode() explicitly. Whether methods return HtmlString (safe under {{ }}) or plain strings (needs {!! !!}) varies by release — dd(Form::text('x')) once, then standardise on one syntax across the codebase rather than mixing.
Checkboxes need a hidden zero. There is no implicit companion input, so an unchecked box posts nothing and $request->boolean('agree') can't distinguish "absent" from "false". Always emit Form::hidden('agree', 0) immediately before Form::checkbox('agree', 1). Same reasoning for multi-selects returning [].
files => true is not automatic. Form::file() will silently produce a broken upload without enctype="multipart/form-data" on the open()/model() call. This is the single most common bug report.
CSRF is version-sensitive. Some builds inject _token inside Form::open()/Form::model(), others require an explicit Form::token(). Verify in the rendered source once; don't rely on memory across upgrades. Excluded URIs in VerifyCsrfToken still apply.
Select/radio "selected" is old-input-first. After a failed save, old() wins over the bound model — that's what you want, but it means the explicit third argument to Form::select() is not authoritative. If you need a hard override, bypass the helper or clear old input.
Numeric-string option keys. PHP casts "0", "1" array keys to integers and "" to 0. A select built from a collection of IDs with a 0 option can render the wrong option as selected. Cast explicitly or build the option list with string-prefixed keys.
Macro return type. A macro that returns null or a non-string produces mangled attribute output mid-tag. Always return new HtmlString(...) or a string.
Debugging. Because everything funnels through HtmlBuilder::attributes(), adding Form::macro('dump', fn() => new HtmlString('<pre>'.e(json_encode(Form::getModel() ...)).'</pre>')) isn't available — instead dd(Form::getSessionKey()), session()->getOldInput(), and the bound model to diagnose wrong prefill. Clear php artisan config:cache and view:cache after editing the published config; stale config is a frequent phantom-bug source.
Extension points, in order of preference: macros → container binding of FormBuilderContract/HtmlBuilderContract with your own subclass → publishing and overriding the compiled views (there are none, so subclassing is the only real route). Prefer the first; subclassing ties you to internal method signatures that will not be maintained.
How can I help you explore Laravel packages today?