reaper / ui
Custom UI components for Laravel using Bootstrap
Requires
- livewire/livewire: ^3.0
This package is auto-updated.
Last update: 2026-07-29 07:30:44 UTC
README
Custom Bootstrap-based UI components for Laravel, plus a Livewire-powered global modal system.
Requirements
- Laravel (tested with the
laravel/laravelskeleton, v10) livewire/livewire^3.0(installed automatically as a dependency)- Alpine.js (ships with Livewire v3) — used by the global modal for open/close transitions
- Bootstrap CSS + Bootstrap Icons — the components emit Bootstrap classes (
btn,btn-primary,bi bi-*, etc.) and expect Bootstrap to be loaded in the host app
Installation
This package is currently distributed as a local path repository, not from Packagist. In the consuming app's root composer.json:
{
"require": {
"reaper/ui": "dev-master"
},
"repositories": [
{
"type": "path",
"url": "packages/reaper/ui",
"options": {
"symlink": true
}
}
]
}
Then install:
composer require reaper/ui:dev-master
The service provider is picked up automatically via Laravel package discovery (declared in composer.json under extra.laravel.providers) — no manual registration needed in config/app.php.
Publishing assets
The package exposes two publish tags:
| Tag | Publishes | Destination |
|---|---|---|
reaper-ui-config |
config/global-modal.php |
config/global-modal.php |
reaper-ui-assets |
resources/js/global-modal.js |
resources/js/vendor/reaper-ui/global-modal.js |
Publish everything:
php artisan vendor:publish --provider="Reaper\Ui\ReaperUiServiceProvider"
Or publish selectively:
php artisan vendor:publish --tag=reaper-ui-config php artisan vendor:publish --tag=reaper-ui-assets
You only need reaper-ui-config if you plan to define global modals (see below) — the file ships empty by default. You need reaper-ui-assets if you plan to use the global modal's trigger helpers from plain HTML/JS (see Opening a modal).
Or use the package's own publish command
The package also ships reaper-ui:publish, a thin wrapper around vendor:publish that's scoped to only this package's files — so it never touches publishables from other packages, unlike a bare php artisan vendor:publish.
# publish everything from reaper/ui php artisan reaper-ui:publish # publish only specific tags php artisan reaper-ui:publish --tag=reaper-ui-config php artisan reaper-ui:publish --tag=reaper-ui-config --tag=reaper-ui-assets # overwrite files that already exist at the destination php artisan reaper-ui:publish --force
What the package registers
On boot, ReaperUiServiceProvider (src/ReaperUiServiceProvider.php):
- Loads package views under the
reaper::namespace (resources/views) - Registers the
<x-reaper-ui::btn>Blade component - Registers the
reaper-ui-modalLivewire component - Registers a test route group under the
reaper-uiprefix/name (routes/web.php— currently just aGET /reaper-ui/testsanity-check route, not meant for app use) - Registers the
reaper-ui:publishArtisan command (console only)
Button component
<x-reaper-ui::btn> renders a Bootstrap button (or an <a> if link is given and resolves to a named route).
<x-reaper-ui::btn text="Save" type="pri.sm" /> <x-reaper-ui::btn text="Delete" type="dan" icon="trash" action="delete" param="{{ $id }}" />
Props
| Prop | Type | Description |
|---|---|---|
text |
string | Button label (alternative to passing the slot) |
type |
string | Dot-separated color.size, see below |
icon |
string | Dot-separated icon-name.position, see below |
action / param |
string | Renders wire:click="{action}('{param}')" |
link |
string | A named route. If set (and it exists via Route::has()), renders an <a href="{{ route(...) }}"> instead of a <button> |
disabled |
bool | Adds the disabled attribute |
stopPropagation |
bool | Adds onclick="event.stopPropagation();" |
Regular Blade attributes (class, id, ...) can be passed normally and are merged onto the root <button> element (not the <a> variant).
type — color and size
Dot-separated, color[.size]:
type="pri.sm"
- Color (required position): one of
pri,sec,suc,dan,war,inf,lig,dar,lin(primary, secondary, success, danger, warning, info, light, dark, link) → addsbtn-{color}. Defaults topriif omitted/unrecognized. - Size (optional, second position):
smorlg→ addsbtn-{size}. Any other value is silently ignored.
icon — icon
Renders a Bootstrap Icon (<i class="bi bi-...">) next to the label.
{{-- shorthand: just the icon name --}} <x-reaper-ui::btn text="Edit" icon="pencil" /> {{-- explicit form: icon + position (start/end, default start) --}} <x-reaper-ui::btn text="Next" icon="arrow-right.end" />
Two names are aliased for backwards compatibility with the host app's original icon set: save → bi bi-floppy, add → bi bi-plus-circle. Any other name is used as-is (bi bi-{name}).
Global Modal
A single Livewire-driven modal shell that can mount any Livewire component inside it, driven either from server-side Livewire code or from a plain DOM click/JS event — useful for opening the same modal from a Blade button, an Alpine component, or fully static HTML without wiring up Livewire on the trigger element itself.
1. Configure the modals you want available
After publishing reaper-ui-config, edit config/global-modal.php:
<?php return [ 'edit-user' => [ 'component-path' => \App\Livewire\Users\EditUserForm::class, // or the Livewire alias string 'header-name' => 'Edit user', // optional, default "Modal" 'header-style' => 'font-weight: 600; font-size: 1rem;', // optional 'max-width' => '600px', // optional, default "1140px" 'stable' => false, // optional, default false — see note below ], ];
Each top-level key (edit-user above) is the modal name you reference when opening it. component-path is the only required field.
stable controls the Livewire wire:key used for the nested component: leave it false (default) so the child component is force-remounted every time the modal opens (fresh state per open, even with the same params); set it true if you want the same params to reuse an existing component instance instead of remounting.
2. Render the modal shell once in your layout
{{-- e.g. resources/views/layouts/app.blade.php, near the end of <body> --}} <livewire:reaper-ui-modal />
3. Load the JS trigger helper (optional — only for non-Livewire triggers)
Publish reaper-ui-assets, then import and initialize it once (e.g. in resources/js/app.js):
import { registerGlobalModal } from './vendor/reaper-ui/global-modal.js'; registerGlobalModal();
This wires up two things globally:
- A
windowlistener for aopen-application-modalCustomEvent - A
documentclick listener for elements matching#global-modalor[data-trigger="global-modal"]
Opening a modal
From a plain button (no Livewire needed on the trigger):
<button data-trigger="global-modal" data-component="edit-user" data-params='{"id": 42}'> Edit user </button>
By dispatching the CustomEvent yourself (e.g. from Alpine or vanilla JS):
window.dispatchEvent(new CustomEvent('open-application-modal', { detail: { modal: 'global', component: 'edit-user', params: { id: 42 } } }));
From server-side Livewire code, dispatch the browser event open-global-modal directly:
$this->dispatch('open-global-modal', component: 'edit-user', params: ['id' => 42]);
In all cases, component must match a key in config/global-modal.php, and params is passed into the mounted component as its params prop/argument.
Inside the mounted component
The component referenced by component-path receives a params array (from mount(array $params) or a public $params property, depending on how you write it). Optionally listen for global-modal-component-ready to run setup logic once it's live in the DOM:
use Livewire\Attributes\On; class EditUserForm extends Component { public array $params = []; #[On('global-modal-component-ready')] public function onReady(): void { // e.g. load the user record now that params are set } }
Closing
The modal closes when the user clicks the backdrop, clicks the × close button, or presses Esc. There's no manual "close" call needed from inside the child component for the common case.
Notes / known limitations
- The
routes/web.phpfile registered under thereaper-uiprefix currently only contains a placeholder test route (GET /reaper-ui/test) — it's not part of the public API and can be ignored. - Only
smandlgsizes are honored on<x-reaper-ui::btn>.