bebo925/tall
TALL Stack preset for Laravel with Blade components for buttons, inputs, panels, tables, modals, messages, and page headings. Install via Composer and run tall:install to scaffold JS deps, Tailwind, Mix, and app layout.
The package is a TALL-stack (Tailwind + Alpine + Livewire) preset plus a set of Blade UI components. You get the scaffolding and pre-styled components in one install.
1. Install
composer require bebo925/tall
php artisan tall:install
tall:install is the workhorse: it installs JS dependencies, writes a tailwind.config.js, touches your build tooling config, and publishes an app.blade.php layout. Back up tailwind.config.js, vite.config.js, and webpack.mix.js before running it — the installer overwrites rather than merges.
2. Fix HMR
Edit the host variable in vite.config.js so hot reloading resolves correctly (especially important if you develop in Docker, Sail, or a VM):
// vite.config.js
server: {
host: 'localhost', // or your container host
},
3. Publish views if you intend to customize
php artisan vendor:publish --tag="tall-views"
Do this on day one if you expect to touch markup. Editing components after you've already built ten screens against the default markup is more churn than publishing up front.
First use case to try: a CRUD form. Wrap your Livewire form fields in <x-tall::input> and drop in <x-tall::messages /> at the top of the layout — this immediately exercises the input wrapper, error binding, and flash messaging in one screen:
<form wire:submit="save">
<x-tall::input label="Name" error="name">
<x-tall::input.text wire:model="name" />
</x-tall::input>
<x-tall::button style="primary" type="submit">Save</x-tall::button>
</form>
Where to look first: the published resources/views/vendor/tall/ directory after vendor:publish. The README documents the call sites well but not the component internals — reading the published Blade files is the fastest way to understand what props and slots actually exist.
The core pattern is a label/error wrapper (x-tall::input) around a field component (x-tall::input.text, .checkbox, .radio, .textarea, .select, .rich-text). The wrapper owns layout, label text, and error display; the field owns the control.
<x-tall::input label="Role">
<x-tall::input.select wire:model="role">
<option value="admin">Admin</option>
<option value="member">Member</option>
</x-tall::input.select>
</x-tall::input>
The error="property_name" prop on the wrapper is what ties the control to a Livewire validation error bag. This is the single most important prop to get right — a typo here means validation messages silently never render (see Gotchas).
components/layouts/app.blade.php ships with <x-tall::messages /> already included. Push notifications from any controller or Livewire action:
session()->flash('message', [
'type' => 'success', // also: warning, danger
'message' => 'Saved successfully!',
]);
For non-redirect flows (e.g. inside a Livewire action that stays on the page), use Livewire's browser event instead — the messages component listens for it:
$this->dispatch('notify', type: 'success', message: 'Saved!');
<x-tall::messages dark="true" /> is available if you run a dark layout.
The package bundles wire-elements/modal and ships a tall-confirmation-dialog component. The idiomatic pattern is dispatching openModal with a confirmation payload:
$this->dispatch('openModal', [
'component' => 'tall-confirmation-dialog',
'arguments' => [
'message' => 'Delete this record? This cannot be undone.',
'title' => 'Warning',
'data' => $recordId,
'confirmText' => 'Delete',
'style' => 'danger',
'event' => 'recordDeleted', // event fired on confirm
],
]);
Then listen for the confirm event in your Livewire component:
#[On('recordDeleted')]
public function destroy($id) { /* ... */ }
This is the highest-leverage pattern in the package — it eliminates hand-rolling per-model confirmation dialogs and keeps the danger styling consistent.
Compose tables from table, table.heading, table.row, table.cell. Use it for admin listings where you want the visual consistency without building a DataTable abstraction:
<x-tall::table>
<x-slot name="head">
<x-tall::table.heading>Name</x-tall::table.heading>
<x-tall::table.heading>Email</x-tall::table.heading>
</x-slot>
<x-slot name="body">
@foreach ($users as $user)
<x-tall::table.row>
<x-tall::table.cell>{{ $user->name }}</x-tall::table.cell>
<x-tall::table.cell>{{ $user->email }}</x-tall::table.cell>
</x-tall::table.row>
@endforeach
</x-slot>
</x-tall::table>
<livewire:tall-markdown-x> is a Livewire component, not a Blade one — bind via :content and pass Tailwind classes per-region via a style array:
<livewire:tall-markdown-x :content="$post->body ?? ''" :style="[
'toolbar' => 'flex items-center justify-between border-b',
'textarea' => 'h-[60vh] p-4 w-full focus:outline-none',
'preview' => 'p-10 prose max-w-none',
'help' => 'p-8 prose max-w-none',
]" />
The style-array pattern is your main theming lever for this component — it avoids forking the view.
<x-tall::multiple-select
:options="Team::all()"
wire:model.defer="selectedTeamIds"
/>
Defaults to id / name for value/label fields. If your model's display column differs, pass valueField / labelField explicitly rather than renaming columns.
<x-tall::dropdown>
<x-slot name="trigger">
<x-tall::button style="secondary">Actions</x-tall::button>
</x-slot>
<x-tall::dropdown.item wire:click="edit">Edit</x-tall::dropdown.item>
<x-tall::dropdown.item wire:click="archive">Archive</x-tall::dropdown.item>
</x-tall::dropdown>
Because the dropdown relies on Alpine, wire:click inside items works but wire:model inside a dropdown item does not — keep dropdown contents action-only.
Build pages from x-tall::page-heading (with meta and actions slots) followed by one or more x-tall::panel blocks (body and footer slots). This establishes a consistent rhythm across an admin app with almost no CSS authoring:
<x-tall::page-heading title="Users">
<x-slot name="actions">
<x-tall::button style="primary" wire:click="create">New User</x-tall::button>
</x-slot>
</x-tall::page-heading>
<x-tall::panel title="All Users">
<x-slot name="body">{{-- table --}}</x-slot>
</x-tall::panel>
The installer is opinionated and destructive. tall:install writes tailwind.config.js and touches build configs. If you already have a Tailwind theme, commit first and diff the results. In particular, custom colors, fonts, and darkMode settings will be lost unless you merge them back.
Vite vs. Webpack Mix is contradictory in the docs. The README says the installer updates webpack.mix.js and tells you to edit vite.config.js for HMR. The package's actual supported path appears to be Vite. If you're on Laravel Mix (Laravel ≤9-era projects), treat this as a full Vite migration task, not a drop-in. Don't assume both toolchains are supported simultaneously.
The README has literal typos you must correct at the call site. The install command line contains //includes https://github.com/wire-elements/modal as an inline comment — do not paste it verbatim into your shell. The panel example closes with </x-tall::panel.body> which does not exist; the correct closing tag is </x-tall::panel>. These aren't stylistic — copying the README blindly produces parse errors and "component not found" exceptions.
error="property_name" must match the Livewire property exactly. This is the #1 source of "my validation messages don't show" bugs. The wrapper doesn't introspect the bound property; it's a manual string match. If you refactor a property name, grep for its error="..." usage.
Validation display depends on Livewire's error bag being scoped to that component. If the form is nested inside a child Livewire component but the wrapper lives in a parent — or the error prop points to a property on a different component — nothing renders. Keep the wrapper and the invalid property in the same component.
Modal components require Livewire. wire-elements/modal is a hard dependency. In an Alpine-only page, $dispatch('openModal', ...) fires into the void. If you're mixing Livewire and non-Livewire pages, gate modal dispatch behind Livewire components and confirm the modal container is mounted in your layout (the package's app.blade.php handles this; a custom layout may not).
rich-text and multiple-select are Livewire components, not Blade components. They're invoked with livewire: prefixes. Mixing the syntaxes (<x-tall::rich-text> vs <livewire:tall-markdown-x>) produces confusing "unable to locate component" errors. Check the README example for each one rather than assuming.
Check Alpine/Livewire load order after install. The installer creates app.blade.php with specific script ordering. If you've merged it into an existing layout and reordered @livewireScripts, @stack('scripts'), or the Alpine CDN/Vite import, dropdowns and modals will break with no console error beyond an Alpine initialization warning. When debugging an unresponsive dropdown, inspect whether Alpine loaded after Livewire.
Publishing views is the primary extension point. There is no documented plugin API, no design-token layer, and no runtime theming. To add a new button style, you publish the views, edit components/button.blade.php, and add a variant to the internal match/switch. Accept this: customization means forking markup, and you own the merge burden on package updates.
Tailwind purging may drop component styles. Components use string-concatenated class names in some spots; if you're on an older Tailwind with a naive purge/content config, classes emitted only from the package's compiled views won't be detected. Ensure content includes the vendor view path (or your published copy) in tailwind.config.js.
Single-maintainer, one-star project — pin the version. With zero dependents and one maintainer, treat upgrades as manual review tasks. Pin an exact version in composer.json, publish the views so you're not coupled to upstream markup, and re-read the released diff before composer update. The upside is a small surface area: the components are mostly presentational Blade, so forking is cheap if the package goes stale.
Debugging checklist when a component renders blank: (1) did you vendor:publish --tag="tall-views" or is the vendor path registered; (2) is the component name spelled with the tall:: namespace; (3) does the slot name match exactly (body, footer, head, meta, actions, trigger) — unnamed content goes to the default slot and will be silently ignored by components expecting named slots.
How can I help you explore Laravel packages today?