Installation & Usage
aaix/laravel-islands mounts self-contained frontend components ("islands") into any Blade or Filament page, hydrated with server-driven props. The core is framework-agnostic; a Vue adapter ships today (a React adapter is planned).
Requirements
- PHP >= 8.3, Laravel 12 or 13
- A working Vite build with the Vue plugin
window.Echoinitialised (only needed for real-time islands)
Installation
composer require aaix/laravel-islands
npm install vue @vitejs/plugin-vueThe service provider is discovered automatically. The frontend half ships as plain sources in the same package — no build step of its own.
Vite
Point the import name at those sources:
// vite.config.js
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
resolve: {
alias: {
'@aaix/laravel-islands': fileURLToPath(
new URL('./vendor/aaix/laravel-islands/resources/js', import.meta.url),
),
},
},
plugins: [laravel({ /* ... */ }), vue()],
});Three entry points hang off that name: @aaix/laravel-islands (framework-agnostic core), @aaix/laravel-islands/vue (the adapter and its composables) and @aaix/laravel-islands/vue/helpers (optional UI, see helpers — those carry Tailwind classes and need an @source line).
App entry
// resources/js/app.js
import { startVueIslands } from '@aaix/laravel-islands/vue';
startVueIslands(import.meta.glob('./islands/**/*.island.vue', { eager: true }));startVueIslands scans the DOM for [data-island][data-island-adapter="vue"] elements, resolves the matching component from the registry you handed it and mounts it. It also re-scans after livewire:navigated, so islands work inside Filament pages.
A registry key is looked up as ./islands/<name>.island.vue first and as ./<name>.island.vue second, so whatever the glob's keys look like decides how deep a name may reach. Feature folders under app/Islands are registered by adding a second glob and normalising its keys:
const featureIslands = Object.fromEntries(
Object.entries(import.meta.glob('../../app/Islands/**/*.island.vue', { eager: true }))
.map(([path, module]) => [`./islands/${path.split('/').pop()}`, module]),
);
startVueIslands({ ...import.meta.glob('./islands/**/*.island.vue', { eager: true }), ...featureIslands });With that normalisation the mount name is the entry file's basename — <x-island name="Products"> finds app/Islands/Products/Products.island.vue, and two islands must not share an entry file name.
Usage
1. Render a mount point
<x-island
name="product-view/ProductView"
:props="['product' => $product->toArray()]"
/>name— path of the island component underresources/js/islands/, without the.island.vuesuffix (product-view/ProductView→resources/js/islands/product-view/ProductView.island.vue).:props— data serialized into the island as initial props.adapter— the frontend adapter, defaults tovue.
2. Write the island component
<!-- resources/js/islands/product-view/ProductView.island.vue -->
<script setup>
defineProps(['product']);
</script>
<template>
<div>{{ product.name }}</div>
</template>Props passed to <x-island :props="..."> arrive as component props. For access to the full island payload (metadata, subscriptions) use useIsland():
<script setup>
import { useIsland } from '@aaix/laravel-islands/vue';
const { props } = useIsland();
</script>Where island components live
Two homes, both registered in the app entry above:
| Home | Mount name | Good for |
|---|---|---|
resources/js/islands/** | the path without the suffix, e.g. product-view/ProductView | a component that is only markup |
app/Islands/<Island>/ | the entry file's basename, e.g. Products | a feature that owns endpoints, queries and state as well |
Either way the file ends in .island.vue. A feature folder is what make:island scaffolds — see island structure.
Real-time on the model
useModel on the frontend keeps a record in sync — the Laravel side needs to broadcast the record's lifecycle onto a channel the runtime can find. The InteractsWithIslands trait wraps that in one line:
use Aaix\LaravelIslands\Concerns\InteractsWithIslands;
class Product extends Model
{
use InteractsWithIslands;
}Under the hood this composes Laravel's own BroadcastsEvents and pins the channel to App.Models.Product.{id} — the same shape <x-island> writes into the mount attributes when you pass :subscribe="$product". created, updated and deleted events go out automatically.
Authorize the channel in routes/channels.php as usual:
Broadcast::channel('App.Models.Product.{id}', fn (User $user, int $id) => true);Nothing else changes on the frontend — the useModel consumer picks up the events off the same private channel and reconciles its reactive state.
Next
- Island structure — what
make:islandscaffolds and where each kind of file belongs. - Composables —
useIsland,useModel,useEcho,useSortableTiles. - Helpers — the optional UI that ships with the package.
- Translations — how
t()gets its lines.