DEV Community

Cover image for I Built Laravel UI Widgets That Don't Need a JavaScript Framework Called Bladewell.
Krishnaprasad R (RK)
Krishnaprasad R (RK)

Posted on Edited on

I Built Laravel UI Widgets That Don't Need a JavaScript Framework Called Bladewell.

Bladewell UI (bladewellui.com) is a free, MIT-licensed library of Blade and Tailwind CSS v4 components for Laravel. Install it from Packagist as bladewell/bladewell.

I've copied the same hand-rolled datepicker between Laravel projects more times than I want to admit. Every time, I'd look at the existing UI kits first, and every time I'd close the tab.

The problem wasn't quality. It was the price of entry. Most Laravel UI kits ask you to adopt their JavaScript along with them: bring Alpine, bring Livewire, or rebuild your frontend in React or Vue. If you're running a plain server-rendered Blade app, that's a significant architectural commitment in exchange for a date field.

So I built Bladewell: 36 Blade + Tailwind CSS v4 widgets, server-rendered, with a small vanilla JS file behind each one.

Copy in, don't depend on

The package is a dev dependency, and the widgets themselves land in your app:

composer require --dev bladewell/bladewell
php artisan bladewell:add datepicker
npm run build
Enter fullscreen mode Exit fullscreen mode

bladewell:add copies the widget's source into your application, along with anything it depends on (the date picker brings its field frame, icons and validation rules like MinimumAge). From that moment it's your code. Rename the classes, rip out half the markup, change the keyboard behaviour. Nobody's stopping you, and nothing of mine runs in production.

This is the shadcn/ui model, and I think it's the right one for UI components specifically. A UI widget is not a hashing library. You will need to change it. The question is whether you're fighting a package's config surface or just editing a file.

The part that usually breaks

Copy-and-own has one well-known failure mode: you own it, so you never get updates again. Six months later you're on a fork of a fork, and the upstream bugfixes may as well not exist.

Bladewell keeps a lock file, bladewell.lock, with a hash of every file it wrote. Commit it like composer.lock. When you update with php artisan bladewell:add --installed:

  • Files you haven't touched get the new version.
  • Files you've edited are skipped and listed in the output, so you know what you're now maintaining yourself.
  • php artisan bladewell:diff shows how your copy differs from the new version, so you can port a fix by hand if you want it.

Nothing is overwritten silently. That was the non-negotiable.

Three constraints I didn't want to compromise on

It has to run under a strict Content Security Policy. No inline <script>, no onclick attributes, no style="", so your policy never needs 'unsafe-inline'. If you've ever tried to retrofit a CSP onto an app full of inline handlers, you know it's a nightmare that never fully ends. Every widget wires itself up from an external JS file via data-* attributes. (The one exception is opt-in: the accordion menu's "remember scroll position" script, which carries your nonce.)

You don't have to take my word for it: the docs site itself is served with default-src 'self' and a per-request nonce, and every live example runs under it. The Why page prints the exact policy it was sent with, so you can hold it against DevTools.

It has to survive Livewire. Alpine and Livewire aren't required, but plenty of people use both, and DOM morphing destroys naive components. The widgets are checked inside Livewire 3 and 4 components in a real browser, not a jsdom approximation: inputs bind with wire:model and keep focus while you type, an open modal or menu stays open through a render, file uploads go to Livewire's temporary uploads, and toasts fired with $this->dispatch() survive wire:navigate.

Accessibility and i18n aren't a later milestone. Keyboard navigation, correct ARIA roles and states, focus management, RTL layouts, and dates and numbers in your app's locale. Every example is checked with axe-core against WCAG 2.2 A and AA, in light and dark, closed and with each popover and dialog opened, on every change. A datepicker that only works with a mouse and only speaks en_US isn't a datepicker, it's a demo.

What's in the set

Datepicker, date-range picker, time picker, select, phone input, OTP and verification-code inputs, file upload, table, pagination, modal, toast, dropdown, tabs, stepper, accordion, tooltips, captcha, and more: 36 in all.

Starting a new app instead? There's a starter kit, a Laravel 13 app with sign-in, registration, password reset, email verification, account settings and a dashboard, built from these components:

composer create-project bladewell/starter-kit my-app
Enter fullscreen mode Exit fullscreen mode

For your coding agent

An angle I didn't expect to care about but now use constantly: a built-in MCP server (php artisan bladewell:mcp), plus /llms.txt and JSON registry endpoints. Your agent can list the widgets, read their props and examples, see what's installed and what you've edited, and run the install with a dry run first. Telling Claude or Cursor "add a date range picker to this form" and having it pick the right widget turns out to be a reasonable workflow.

Where it isn't a fit

Being honest saves you an afternoon: it's not for Tailwind v3 or Bootstrap projects, not for Inertia apps whose pages are React or Vue, and not if you'd rather never see a component's code. An installed package you only composer update suits that better.

Try it

Live previews, props and source for every widget: https://www.bladewellui.com/

PHP 8.3+, Laravel 12 and 13, Tailwind CSS 4.1+. Free and MIT licensed. Feedback and issues welcome on GitHub.

Top comments (0)