hamzi / catchy
Convert standard Laravel applications into SPAs using Alpine.js and HTML-over-the-wire.
Requires
- php: ^8.2
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0
- phpunit/phpunit: ^10.0 || ^11.0
README
A featherweight Single Page Application (SPA) adapter for Laravel 11, 12 & 13
Laravel Catchy empowers you to convert standard Laravel applications into SPAs using Alpine.js and the @alpinejs/morph plugin.
No complex JavaScript builds. No routing overhead. 100% SEO-friendly. Dynamic Head/Meta updates. Script tag execution. Dynamic translation and native RTL support.
Features
- Featherweight footprint: Pure Alpine.js plugin and a thin Laravel middleware.
- HTML-over-the-wire: Only transfers layout fragments over the network, dramatically saving bandwidth.
- Dynamic SEO/Head Merging: Automatically syncs incoming
<head>elements (meta description, keywords, OpenGraph, dynamic styles) to maintain 100% SEO parity. - Script Tag Execution: Intercepts and executes dynamic
<script>elements found inside the morphed content automatically. - Form submission interception: Intercepts
GETandPOSTforms natively (including CSRF tokens and Laravel_methodspoofing). - Logical LTR/RTL Layouts: UI Components are designed using Tailwind logical properties (
start,end,ms-,me-,text-start) to support both LTR and RTL directions seamlessly out-of-the-box. - Deferred / Lazy Component Loading: Lazy load heavy components (
<x-catchy-lazy>) when they enter the viewport usingIntersectionObserver. - Intelligent Viewport Prefetching: Prefetch links dynamically in the background as they enter the viewport using
data-catchy-prefetch="viewport". - Scroll Retention Control: Easily bypass the default scroll-to-top behavior on transitions using
data-catchy-scroll="keep". - Inline Validation Error Management: Render dynamic field error elements (
<x-catchy-error>) that display Laravel validation messages on the fly. - Localization Integration: Translatable component strings, fully customizable via standard Laravel language publishing.
- Optimized Caching & CDN: Offers in-memory directive caching and asset publishing to let the browser cache the script file, saving ~40KB of HTML payload per page load.
- Graceful degradation: Seamlessly falls back to regular page loads on server errors, external redirects, or slow connections.
Installation
Install the package via Composer:
composer require hamzi/catchy
Note: In Laravel 11.x, 12.x, and 13.x, the package's service provider registers automatically.
Configuration
1. Register the Middleware
Apply the catchy middleware to the routes you want to behave as an SPA.
For Laravel 11.x/12.x/13.x, register the middleware globally or inside the web middleware group in bootstrap/app.php:
use Hamzi\Catchy\Http\Middleware\CatchySPAMiddleware; ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ CatchySPAMiddleware::class, ]); })
Alternatively, you can apply it only to specific routes/groups using the catchy alias:
Route::middleware('catchy')->group(function () { Route::get('/', [HomeController::class, 'index']); Route::get('/about', [AboutController::class, 'show']); });
2. Publish Configuration (Optional)
If you wish to change the default HTML container wrapper ID (catchy-app) or prefetching thresholds:
php artisan vendor:publish --tag=catchy-config
3. Publish Assets for Caching (Highly Recommended)
To prevent inlining JavaScript in every page response and allow browsers to cache the core script, publish the JS asset:
php artisan vendor:publish --tag=catchy-assets
This serves Catchy via <script src="/vendor/catchy/catchy.js">, reducing the HTML size of every page request by ~40KB.
4. Publish Translations (Optional)
To customize or translate the component text labels (e.g. for multi-language apps):
php artisan vendor:publish --tag=catchy-translations
Setup & Frontend Integration
Catchy can be loaded dynamically via Blade directives (CDN mode) or bundled locally using modern asset managers like Vite (NPM mode).
Method A: CDN/Blade Directive (Zero Configuration)
If you prefer not to compile assets via NPM, simply add the @catchyScripts Blade directive before the closing </body> tag in your layout. By default, it will automatically inject @alpinejs/morph from UNPKG CDN and initialize the plugin.
Ensure your layout has Alpine.js loaded:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Method B: NPM/Vite Bundler (Recommended for Production)
- Turn off CDN morph auto-injection in
config/catchy.php:
'include_morph' => false,
- Install Alpine.js, the morph plugin, and Catchy via NPM:
npm install alpinejs @alpinejs/morph npm install hamzi-catchy
- Import and register the plugins in your
resources/js/app.js:
import Alpine from 'alpinejs'; import morph from '@alpinejs/morph'; import Catchy from 'hamzi-catchy'; Alpine.plugin(morph); Alpine.plugin(Catchy); window.Alpine = Alpine; Alpine.start();
Reusable Blade UI Components
Catchy includes professionally styled, logical RTL/LTR, translatable Blade components.
1. Spinner (<x-catchy-spinner />)
An animated SVG spinner for inline loaders or buttons:
<x-catchy-spinner /> <x-catchy-spinner size="sm" color="accent" />
Options: size (xs, sm, md, lg, xl) · color (primary, accent, white, gray)
2. Skeleton Loader (<x-catchy-skeleton />)
<x-catchy-skeleton type="title" /> <x-catchy-skeleton type="text" lines="3" /> <x-catchy-skeleton type="circle" /> <x-catchy-skeleton type="card" />
3. Fade-in Transition (<x-catchy-fade />)
<x-catchy-fade duration="350"> <div class="card">Content fades in smoothly.</div> </x-catchy-fade>
4. SPA Form (<x-catchy-form />)
Automates CSRF tokens, method spoofing, and lifecycle callbacks:
<x-catchy-form action="/profile" method="PUT" beforesend="loading = true" success="loading = false; alert('Saved!')" error="loading = false"> <input type="text" name="name"> <button type="submit">Save</button> </x-catchy-form>
5. Modal Dialog (<x-catchy-modal />)
Responsive modal with backdrop blur, scale transitions, keyboard closing, and automatic text-start direction alignment:
<!-- Place once in your layout --> <x-catchy-modal id="my-modal" size="lg" /> <!-- Trigger via SPA link --> <a href="/users/1/edit" data-catchy-modal>Edit User</a>
6. Toast Notifications (<x-catchy-toast />)
Displays session flash messages and dynamic SPA notifications with support for logical alignments (top-right resolves to top-right on LTR but top-left on RTL):
<!-- Place once in your layout --> <x-catchy-toast position="top-right" duration="4000" />
7. Validation Error Indicator (<x-catchy-error />)
Automatically shows dynamic validation messages inline when validation fails on a form submit without full page reloads:
<label for="email">Email Address</label> <input type="email" name="email" id="email"> <!-- Displays error message for the 'email' input on form validation failure --> <x-catchy-error field="email" class="text-rose-500 text-xs mt-1" />
8. Lazy Component Loader (<x-catchy-lazy />)
Allows you to load heavy elements (e.g. dynamic cards, charts, dashboards) asynchronously.
<!-- Renders placeholder, then immediately loads contents from /widgets/activity --> <x-catchy-lazy src="/widgets/activity" /> <!-- Lazy loads content only when the element is scrolled into view (using IntersectionObserver) --> <x-catchy-lazy src="/widgets/recent-stats" trigger="intersect" /> <!-- With custom placeholder --> <x-catchy-lazy src="/widgets/orders" trigger="intersect"> <x-slot:placeholder> <div class="p-4 bg-gray-100 rounded-lg text-center animate-pulse">Loading orders...</div> </x-slot:placeholder> </x-catchy-lazy>
9. Off-Canvas Drawer (<x-catchy-offcanvas />)
A slide-in sidebar panel that enters from viewport edges (left, right, start, end, top, bottom), styled with modern Tailwind CSS:
<!-- Place once in your layout --> <x-catchy-offcanvas id="my-drawer" title="Filters" direction="right" /> <!-- Trigger via SPA link --> <a href="/filters" data-catchy-offcanvas="my-drawer">Open Filters Drawer</a>
Options:
id(string): The DOM element ID. Defaults tocatchy-offcanvas.title(string): Header text.direction(left|right|start|end|top|bottom): Viewport entry direction (RTL/LTR logical property safe).closeOnOutsideClick(boolean): Closes when clicking the backdrop. Defaults totrue.
10. Button (<x-catchy-button />)
A highly customizable button featuring color variants, size variants, hover transitions, and automatic loading spinner integration:
<x-catchy-button variant="primary" size="md">Save Changes</x-catchy-button> <x-catchy-button variant="danger" size="sm" :loading="false">Delete</x-catchy-button>
Options:
type(button|submit): HTML button type. Defaults tobutton.variant(primary|secondary|success|danger|outline|ghost): Defaults toprimary.size(sm|md|lg): Defaults tomd.loading(boolean): Iftrue, the button inherits loading state from the parent intercepting form and automatically shows a spinner during SPA submission transitions. Defaults totrue.
11. Card (<x-catchy-card />)
A structured content container featuring optional hover scaling and transitions, fully dark-mode friendly:
<x-catchy-card hoverable> <x-slot:header> <h3 class="font-semibold text-slate-900 dark:text-white">Card Title</h3> </x-slot:header> <p class="text-slate-600 dark:text-slate-400">This is the main card body content.</p> <x-slot:footer> <span class="text-xs text-slate-400">Last updated 5m ago</span> </x-slot:footer> </x-catchy-card>
Options:
hoverable(boolean): Adds scale-up hover animation and card border highlight. Defaults tofalse.
12. Alert Banner (<x-catchy-alert />)
A dismissible feedback banner for success, warning, or error information with Alpine-based fade transitions:
<x-catchy-alert type="success" :dismissible="true"> Your profile has been updated successfully! </x-catchy-alert>
Options:
type(success|danger|warning|info): Defaults toinfo.dismissible(boolean): Shows a close button to fade out the alert. Defaults totrue.
13. Badge (<x-catchy-badge />)
A small tag component for status indicators and metadata labels:
<x-catchy-badge variant="success" rounded>Active</x-catchy-badge> <x-catchy-badge variant="danger" size="sm">Failed</x-catchy-badge>
Options:
variant(primary|secondary|success|danger|warning|info): Defaults toprimary.size(sm|md): Defaults tomd.rounded(boolean): Uses fully rounded (pill) border-radius if set totrue. Defaults tofalse.
14. Dropdown Menu (<x-catchy-dropdown />)
An Alpine.js-powered dropdown wrapper that handles toggling, outside clicks, and logical direction configurations:
<x-catchy-dropdown align="end" width="w-48"> <x-slot:trigger> <button class="px-4 py-2 bg-slate-100 rounded-lg">Options</button> </x-slot:trigger> <x-slot:content> <a href="/profile" class="block px-4 py-2 hover:bg-slate-50 dark:hover:bg-slate-800">Profile</a> <a href="/settings" class="block px-4 py-2 hover:bg-slate-50 dark:hover:bg-slate-800">Settings</a> </x-slot:content> </x-catchy-dropdown>
Options:
align(left|right|start|end): Position of dropdown menu relative to the trigger. Defaults tostart(RTL/LTR direction logical property friendly).width(string): Tailwind width class. Defaults tow-48.
15. Input Field (<x-catchy-input />)
A standard form text input component equipped with modern styling, labels, and automatic inline validation error messages:
<x-catchy-input name="email" type="email" label="Email Address" placeholder="you@example.com" required helper="We'll never share your email." />
Options:
name(string, required): The input field name and ID.label(string): Text for the field label. Shows a red*symbol ifrequiredis true.type(string): The input type (e.g.text,email,password,number). Defaults totext.placeholder(string): Placeholder text.value(string): Initial field value.required(boolean): Marks input field as required. Defaults tofalse.helper(string): Additional helper text printed underneath the input field.- Note: This component automatically embeds the
<x-catchy-error :field="$name" />inline warning tag, meaning any Laravel validation failure on this field will display the error instantly.
Advanced Options & APIs
Declarative Trigger Actions (Modal & Drawer Actions)
Catchy allows you to open or close Modals and Off-Canvas Drawers declaratively from click actions or upon form submissions success/failure without writing any Alpine or custom JS.
1. On Click Actions
Add these attributes to buttons, links, or any clickable element to toggle modals or drawers:
data-catchy-open-modal="modal-id": Opens the specified modal.data-catchy-close-modal="modal-id": Closes the specified modal.data-catchy-open-offcanvas="drawer-id": Opens the specified drawer.data-catchy-close-offcanvas="drawer-id": Closes the specified drawer.
Example:
<!-- Opens a modal on click --> <button data-catchy-open-modal="auth-modal" class="btn">Login</button> <!-- Closes the modal from inside or outside --> <button data-catchy-close-modal="auth-modal">Cancel</button>
2. On Form Success / Error Actions
Apply these attributes directly to form tags (e.g. <x-catchy-form> or a regular form) to trigger components automatically depending on the request outcome:
data-catchy-success-open-modal="modal-id": Opens modal when form submits successfully.data-catchy-success-close-modal="modal-id": Closes modal when form submits successfully (ideal for editing forms inside modals).data-catchy-success-open-offcanvas="drawer-id"/data-catchy-success-close-offcanvas="drawer-id": Triggers off-canvas on successful form submit.data-catchy-error-open-modal="modal-id": Opens error details modal on failure.
Example:
<!-- Form inside a modal that automatically closes the modal on success --> <x-catchy-form action="/update-profile" method="POST" data-catchy-success-close-modal="edit-profile-modal"> <input type="text" name="name" required> <button type="submit">Update</button> </x-catchy-form>
3. Advanced Form & Lazy Reload Actions
Catchy provides additional helper triggers to reset forms, display custom alerts/toasts, and reload lazy components reactively upon request outcomes:
data-catchy-success-reset: Automatically resets all form fields upon successful form submission (ideal for message/comment forms).data-catchy-success-toast="message": Triggers a success toast notification directly.data-catchy-error-toast="message": Triggers an error toast notification directly on failure.data-catchy-success-reload="lazy-id": Automatically reloads/refreshes the<x-catchy-lazy>component with the corresponding ID (ideal for updating dashboards, statistics, or comment sections reactively).data-catchy-error-reload="lazy-id": Reloads the specified lazy component on failure.
Example:
<!-- Submitting this comment form will: 1. Post the data asynchronously. 2. Reset/clear the input fields on success. 3. Pop up a success toast notification. 4. Refresh the lazy comment list component (id="comment-list"). --> <x-catchy-form action="/comments" method="POST" data-catchy-success-reset data-catchy-success-toast="Comment posted successfully!" data-catchy-success-reload="comment-list" > <textarea name="comment" required></textarea> <button type="submit">Submit</button> </x-catchy-form> <!-- Lazy component observing and listing comments --> <x-catchy-lazy id="comment-list" src="/comments/list" />
Real-Time Data Syncing (x-catchy-sync)
Catchy provides an Alpine.js directive called x-catchy-sync to sync form or input data with the Laravel backend in real-time, making it extremely easy to build auto-saving inputs, dynamically filtered lists, and live search forms (similar to Livewire's wire:model or HTMX).
Directive Syntax & Modifiers
x-catchy-sync="url": Posts input value to the specified URL when it changes..input: Triggers syncing on every keystroke (inputevent) instead of on lose focus (changeevent)..debounce.Xms: Delays submission of the input event byXmilliseconds..form: Serializes and posts the entire parent form instead of just the single input..target.container-id: Morphs the returned HTML into the target element with IDcontainer-id.
Example 1: Real-time Live Search
Submits search query on every keystroke (debounced by 300ms) and updates the search results list dynamically:
<input type="text" name="query" placeholder="Search products..." x-catchy-sync.input.debounce.300ms.target.search-results="/products/search" > <!-- Container that will be morphed with the response from /products/search --> <div id="search-results"> <!-- Search results list --> </div>
Example 2: Form Auto-Saving
Submits the entire form state in the background whenever a change is detected:
<form action="/profile/autosave" method="POST"> @csrf <input type="text" name="bio" x-catchy-sync.form="/profile/autosave"> <input type="checkbox" name="notifications" x-catchy-sync.form="/profile/autosave"> </form>
Action Confirmation (data-catchy-confirm)
To prevent accidental clicks on destructive actions (e.g. deleting items or leaving unsaved forms), add data-catchy-confirm="message" to any link or form. Catchy will automatically display a confirmation dialog before proceeding:
<!-- Confirms link navigation --> <a href="/delete-account" data-catchy-confirm="Are you sure you want to permanently delete your account? This cannot be undone.">Delete Account</a> <!-- Confirms form submissions --> <x-catchy-form action="/settings/reset" method="POST" data-catchy-confirm="Are you sure you want to reset settings to default?"> <button type="submit">Reset Settings</button> </x-catchy-form>
Custom Modal Confirmation (data-catchy-confirm-modal)
If you want to use a custom styled modal (instead of browser native confirm() popups) for confirmations:
data-catchy-confirm-modal="modal-id": Placed on a form or link to open the specified modal.data-catchy-confirm-button: Placed on the "Confirm/Yes" button inside your modal to proceed with the action.
Example:
<!-- Intercepts submit and opens custom modal --> <x-catchy-form action="/delete-photo" method="POST" data-catchy-confirm-modal="delete-modal"> <button type="submit">Delete Photo</button> </x-catchy-form> <!-- Your Custom Confirmation Modal --> <x-catchy-modal id="delete-modal" title="Confirm Delete"> <p>Are you sure you want to delete this photo?</p> <div class="mt-4 flex gap-3"> <!-- Close button (standard Alpine/modal close) --> <button type="button" @click="close()">Cancel</button> <!-- Proceed button (marked with data-catchy-confirm-button) --> <button type="button" data-catchy-confirm-button class="bg-red-600 text-white">Confirm</button> </div> </x-catchy-modal>
Connectivity & Offline Protection
Catchy monitors client network status automatically.
- Offline Warnings: If a user goes offline or online, Catchy dynamically fires success/error toasts warning the user of their connection status.
- Request Interception: If a user attempts to click a link or submit a form while offline, Catchy intercepts the request early, halts the page crash, and prompts a friendly "Cannot navigate. You are currently offline" alert.
Automatic Focus Restoration (autofocus support)
When pages transition in standard SPAs, the focus is lost. Catchy brings back native browser behaviors by scanning the morphed container and automatically focusing the first input with an autofocus or data-catchy-autofocus attribute.
Scroll Position Control
By default, Catchy scrolls the viewport to top on page transition. You can keep the current scroll position by adding data-catchy-scroll="keep" to links or form elements:
<a href="/tab/2" data-catchy-scroll="keep">Open Tab 2</a>
Viewport Prefetching
Catchy can prefetch pages dynamically when links enter the viewport (similar to modern static-site generators). Add data-catchy-prefetch="viewport" to enable viewport-level prefetching on an anchor:
<a href="/heavy-page" data-catchy-prefetch="viewport">Heavy Dashboard</a>
Programmatic Loader Controls
You can programmatically trigger the global loading progress bar using Alpine's exposed global space:
// Start the loader Alpine.catchy.startLoading(); // Stop/finish the loader Alpine.catchy.stopLoading();
License
The MIT License (MIT). Please see License File for more information.