Mounting Islands
<x-island> is the only thing a page needs to know about an island. It renders the mount element the runtime looks for, and carries everything the island starts with.
<x-island
name="ShopOrders"
:props="$islandProps"
:subscribe="$order"
/>Attributes
| Attribute | Type | Default | Purpose |
|---|---|---|---|
name | string | required | The registry key of the component — see Resolving the Component. |
:props | array | [] | Serialised into the payload. Arrives both as component props and under useIsland().props. |
:subscribe | Model, array<string, Model> or null | null | Models the island should keep in sync — see Real-Time Models. |
adapter | string | vue | The frontend adapter that mounts this element — see Custom Adapters. |
Every prop must be JSON-serialisable. Pass arrays and scalars; turn a model into an array first, ideally through a presenter so only the fields the island draws cross the wire.
The Rendered Element
<div data-island="ShopOrders" data-island-adapter="vue" data-island-payload="{…}"></div>The payload is one JSON object: props is yours; _island carries the subscriptions (channel and event names per model), the translation lines and the locale, and is read by useModel() and useTranslations(). Once mounted, the runtime adds data-island-mounted.
Resolving the Component
startVueIslands(registry) receives a map of keys to modules, or to loaders when the entry is lazy — the shape both @aaix/laravel-islands/islands and import.meta.glob() produce. A loader is wrapped in defineAsyncComponent(), so the island is fetched when the page mounts it. For a name, it tries two keys in order:
./islands/<name>.island.vue./<name>.island.vue
The registry from the installation keys every island by its entry file name, so app/Islands/Products/Products.island.vue is mounted as name="Products". A glob added by hand keeps the paths it produces: a lone component under resources/js/islands/product-view/ProductView.island.vue is mounted as name="product-view/ProductView".
When no key matches, the runtime logs [islands] vue component not found: "…" and leaves the element empty. When the element names an adapter nobody registered, it logs [islands] no adapter registered for "…". Both are warnings, not errors — the rest of the page keeps working.
Islands in Filament
A Filament custom page is a natural host. Return an empty heading, and let the island draw its own:
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Islands\Products\ProductsProps;
use Filament\Pages\Page;
class ProductsPage extends Page
{
protected string $view = 'islands.products';
public function getHeading(): string
{
return '';
}
protected function getViewData(): array
{
return ['islandProps' => app(ProductsProps::class)->build(request())];
}
}{{-- resources/views/islands/products.blade.php --}}
<x-filament-panels::page>
<x-island name="Products" :props="$islandProps" />
</x-filament-panels::page>The mount element carries wire:ignore, so a Livewire re-render of the surrounding component morphs around it instead of through it — without that, the server's empty mount element would replace everything the island has drawn. Keep the <x-island> tag inside a plain <div> so Livewire has a stable node to diff.
Navigating Without a Page Load
With Filament's ->spa() — or any wire:navigate link — Livewire swaps the body instead of loading a page, and the runtime follows along:
- On
livewire:navigatingevery island is unmounted before the swap. Composables clean up behind it: Echo channels are left, window listeners and history handlers removed. Livewire stores the page for the back button right after that point, so the snapshot holds empty mount elements rather than a dead copy of the rendered islands. - On
livewire:navigatedthe islands of the new page mount, and an island whose element a Livewire morph removed in between is unmounted. - A back or forward step that Livewire restores from its snapshot mounts the islands again from the restored markup.
- An island that hands the runtime state through
useIslandState()gets it back when the same URL is shown again — after a back step, after a link to a page seen before, and after a reload or a direct visit, since the runtime also keeps the last ten pages inlocalStoragefor a day — so it paints its first frame with what the user last saw and refreshes behind it. After a back or forward step the runtime also puts the scroll position back once the islands have mounted; Livewire's own restore runs before they exist. The datagrid does this for its rows out of the box, andrestore: falseturns it off for a table.
Nothing has to be configured. Without navigate events the runtime behaves as on a classic page load.
Links an island renders
A link inside an island is markup Vue owns, so it carries no wire:navigate and loads a whole document — every row that points at a record does. delegateNavigate() from the core entry answers that in one place instead of per view:
import { delegateNavigate } from '@aaix/laravel-islands';
delegateNavigate(document, { within: '/admin' });It swaps a plain left click on a link whose path lies inside within and leaves everything else to the browser: a new tab, a modifier click, download, a foreign host, mailto:/tel:, a link that already has wire:navigate, and anything under data-no-spa — which is what a page that needs its own document, an embedded tool or a log viewer, is marked with. Without Livewire on the page it does nothing at all.
A view that navigates on its own — a whole card as one target, with no link to catch — reaches the same mechanism through navigateTo(url), which falls back to an ordinary page load:
import { navigateTo } from '@aaix/laravel-islands';
navigateTo(row.url);
``` A datagrid's history entries carry Livewire's navigation state as well, so a
back step into a filtered table works from another page, and a step that changes only the
table's own parameters is answered by the table itself — see
[Table State](https://aaix.github.io/laravel-islands-datagrid/table-state#the-url).
::: tip Multiple islands per page
A page may carry any number of islands. Each is its own Vue application — a dashboard
built from four widgets is four islands, and one of them failing to resolve does not affect
the other three.
:::
## The Setup Hook
Because every island is a separate Vue application, plugins, global components and
application-wide provides must be registered per app. `startVueIslands()` accepts a
`setup` callback that runs for each island before it mounts:
```js
// resources/js/app.js
import { startVueIslands } from '@aaix/laravel-islands/vue';
startVueIslands(registry, {
theme: { button: { shape: 'pill' } },
setup(app, payload) {
app.config.errorHandler = (error) => reportToSentry(error, payload);
},
});app is the Vue application instance, payload the parsed island payload. Whatever a Vue plugin would normally do in main.js belongs here. The look of the helpers is not a plugin but a theme — see Theming.
Other Frameworks
The core is framework-agnostic. registerAdapter(name, (element, payload) => …) from @aaix/laravel-islands registers a mount function under a name, <x-island adapter="react"> selects it, and startIslands() scans the page. The payload shape is the one shown above; createEchoController() gives an adapter the same channel handling the Vue composables use.