foxws/wireui
Foxws WireUI is a Laravel and Livewire UI component library with ready-made Tailwind-styled Blade components such as buttons, modals, notifications, forms, and inputs. It helps you build interactive interfaces faster with less custom markup.
Install and wire up basics.
composer require foxws/wireui
php artisan wireui:install
The install command publishes config/wireui.php, the frontend assets, and (optionally) the blade component views. If it doesn't run automatically, publish manually:
php artisan vendor:publish --tag=wireui:config
php artisan vendor:publish --tag=wireui:assets
php artisan vendor:publish --tag=wireui:components # only if you plan to override
Then in your layout (resources/views/layouts/app.blade.php), you need four things in this order:
// tailwind.config.js
content: [
'./resources/**/*.blade.php',
'./vendor/foxws/wireui/resources/**/*.blade.php',
'./vendor/foxws/wireui/src/View/**/*.php',
],
<head> / before </body>:
@wireUiScripts
@livewireStyles / @livewireScripts.First use case — notifications, because it's the smallest surface and validates the whole toolchain. From a Livewire component:
$this->notification()->success('Saved', 'Record updated.');
or from a controller:
Notification::success('Saved')->send();
If the toast appears with correct colours and dismisses cleanly, your Tailwind content paths, Alpine load, and script tag are all correct. That's the smoke test before you touch modals or form components.
Where to look first:
config/wireui.php — notification position, colour palettes, modal defaults, dark-mode class.vendor/foxws/wireui/src/View/Components/ — every available <x-...> component and its props.resources/views/components/ in the vendor dir — copy any view you want to override into resources/views/vendor/wireui/components/.The core value is that every interactive primitive is a Blade component you don't have to write JS for:
<x-button primary label="Save" wire:click="save" />
<x-input wire:model="email" label="Email" />
<x-select wire:model="role" :options="$roles" />
<x-datetime-picker wire:model="starts_at" />
<x-modal-card title="Edit user" wire:model="showModal"> ... </x-modal-card>
Because these are Blade components (not Livewire components), they render server-side and hydrate through Alpine — no extra roundtrip, no separate mount lifecycle. Treat them as "styled HTML with Alpine behaviour" and compose them freely inside your own Livewire views.
The package exposes Livewire traits/helpers so you never hand-roll $dispatch('open-modal'):
use WireUi\Traits\WireUiActions;
class DeleteUser extends Component
{
use WireUiActions;
public function confirmDelete()
{
$this->dialog()->confirm([
'title' => 'Delete user?',
'description' => 'This cannot be undone.',
'accept' => ['label' => 'Delete', 'method' => 'destroy'],
'reject' => ['label' => 'Cancel'],
]);
}
public function destroy() { /* ... */ }
}
The method key round-trips back to your Livewire component — this is the pattern to standardise on for destructive actions. It keeps confirmation logic on the server, avoids duplicated JS, and makes the flow testable with Livewire's testing helpers (dispatch the accept event and assert).
Wire both notification channels from the start:
$this->notification()->success(...). Use this inside components after an action completes.Notification::success(...)->send() flashes to session and the <x-notifications> component (which must be in your layout) drains it on next render.Keep a small Notifier service or trait wrapping these so the rest of your app doesn't import package classes directly, and so swapping the package later is a one-file change.
These components integrate with Livewire's @error bag automatically. The main integration tip is to not fight it — let the component render errors, and only override the view when you need a different error slot:
<x-input wire:model.blur="email" label="Email" />
{{-- error shows automatically; no @error block needed --}}
Use wire:model.blur (not wire:model) on text inputs to reduce chatter, and debounce on search fields where the package's select/datatable hooks expect it.
Put in your base layout once, and forget it:
<html class="{{ config('wireui.dark_mode') ? 'dark' : '' }}">
<head>
@vite(['resources/css/app.css'])
@wireUiScripts
@livewireStyles
</head>
<body>
{{ $slot }}
<x-notifications /> {{-- required for backend-sent toasts --}}
<x-dialog /> {{-- required for confirm/dialog helpers --}}
@livewireScripts
</body>
</html>
The <x-notifications> and <x-dialog /> mounts are the #1 omission — helper calls silently no-op without them.
Alpine is a hard dependency and version-sensitive. The package is built against Alpine 3. If you're on Livewire 3 you're fine (it bundles Alpine). If you're on Livewire 2, load Alpine before @livewireScripts and do not load it twice — duplicate Alpine loads cause "Alpine: Cannot read property 'start' of undefined" and modals that open-and-instantly-close.
Tailwind content paths are the most common "it looks broken" bug. Miss the vendor path and you get unstyled toasts, invisible modal backdrops, and dropdowns with no positioning. If a component renders but looks wrong after a production build, this is the first thing to check — the dev build may have been picking it up via a broader glob that the production purge doesn't.
Re-publish after upgrading. php artisan vendor:publish --tag=wireui:assets --force after composer updates, then clear view:clear and config:clear. Stale published views are a silent source of "the fix didn't take." In CI, either don't publish (use the vendor views) or always publish with --force.
Modals and Livewire re-renders. A modal whose wire:model state lives in the Livewire component will be re-mounted on every render, which resets Alpine state inside it. If you have a nested Livewire component inside a modal, give it a stable wire:key or the modal will visibly flicker/reset on each keystroke in a sibling field.
Z-index and stacking contexts. Modals and notifications use fixed positions. If they appear behind your app shell, it's usually a transform/filter/will-change on an ancestor creating a stacking context. Fix by mounting <x-notifications>/<x-dialog> at </body> level rather than inside a container with overflow-hidden.
Customising components — prefer publishing single views over editing vendor files. Copy one file into resources/views/vendor/wireui/components/, then run php artisan view:clear. Bulk-editing vendor views survives until the next composer update and then breaks silently.
Config quirks. Notification position is wireui.notifications.position (top-right / bottom-left, etc.). Dark mode is opt-in via wireui.dark_mode = true and a dark class on <html> if you use Tailwind's class strategy — toggling one without the other gives you either no dark styles or dark styles on a light page.
Debugging a "nothing happens" click. In order: (1) is @wireUiScripts present? (2) is <x-dialog /> mounted? (3) open the browser console — Alpine will log "Component not found" if a directive fails to register. (4) php artisan view:clear and hard-reload; cached views hide directive changes.
Extension points to know. The WireUiActions trait is the intended surface for Livewire-side helpers; WireUi\View\Components\* classes are for extending a single component; events (wireui:confirm, wireui:notification-sent) are dispatched on window, so you can listen from your own JS for analytics or chained flows without patching the package.
Testing. In Livewire tests, notification calls don't assert cleanly unless you Notification::fake() (if the package wires the standard contract) or assert on $component->effects['dispatches']. Dialog confirmations are easier to test as two units: assert the dispatch happened, then call the accept method directly — don't try to simulate the Alpine click.
How can I help you explore Laravel packages today?