v1 LiVue v1 is here — server-driven reactivity for Laravel using Vue.js Get Started →

Rendering

Blade directives, component tag syntax, inline Vue Composition API with @script, and Teleport support.

Blade Directives

LiVue provides several Blade directives to render components and manage their assets in your layouts and views.

@livue

The primary directive for rendering a LiVue component. Pass the component name as the first argument and an optional array of props as the second.

resources/views/welcome.blade.php
<!-- Basic usage -->
@livue('counter')

<!-- With props -->
@livue('counter', ['count' => 10])

<!-- With dynamic props -->
@livue('user-profile', ['userId' => $user->id])

@livueScripts

Loads the LiVue JavaScript runtime bundle along with any component-declared scripts (#[Js]). Place it before the closing </body> tag.

<body>
    {{ $slot }}

    @livueScripts
</body>

@livueStyles

Injects the CSS declared by components via #[Css] attributes into the <head>.

<head>
    <meta charset="utf-8">
    @livueStyles
</head>

@livueLoadStyle

Loads a stylesheet previously registered with ->onRequest(). Use it inside the Blade view that actually needs the asset. Signature: @livueLoadStyle('id', 'package') where package is optional (default: app).

@livueLoadStyle('table-css', 'primix/tables')
@livueLoadStyle('app-only-css')

@livueLoadScript

Loads a JavaScript asset registered with ->onRequest(). Signature: @livueLoadScript('id', 'package', options) where package defaults to app. Supports optional script attributes (type, defer, async).

@livueLoadScript('table-js', 'primix/tables')
@livueLoadScript('app-only-js')
@livueLoadScript('table-js', 'primix/tables', [
    'type' => 'module',
    'defer' => false,
    'async' => true,
])

Execution timing — On full page responses, LiVue hoists on-request loaders to <head> so CSS starts early. During SPA navigation, loaders stay in the swapped body and are replayed by the runtime before component reboot.

@livueHead

Renders dynamic head elements managed by your page components, such as meta tags, canonical URLs, and JSON-LD structured data. These are defined via the $head property or the head() method on your component.

<head>
    <meta charset="utf-8">
    @livueHead
    @livueStyles
</head>

Complete Layout Example

Here is the recommended layout structure with all LiVue directives in place:

resources/views/components/layouts/app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    @livueHead
    @livueStyles

    <!-- livue-assets -->

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    {{ $slot }}

    @livueScripts
</body>
</html>

Tip — When inject_assets is enabled in config/livue.php (the default), LiVue automatically injects scripts and styles when it detects components on the page. In that case, the manual Blade directives are optional.

Component Tag Syntax

As an alternative to the @livue directive, you can render components using a self-closing HTML-like tag syntax. Both approaches produce identical output and can coexist in the same template.

template.blade.php
<!-- Basic component -->
<livue:counter />

<!-- Static props (passed as strings) -->
<livue:counter count="10" label="My Counter" />

<!-- Dynamic props (PHP expressions) -->
<livue:counter :count="5" />
<livue:counter :count="$initialCount" />
<livue:counter :items="$collection->toArray()" />

<!-- Boolean props -->
<livue:toggle active />

Static vs Dynamic Props

Syntax Value Passed Example
prop="value" String 'value' count="10" passes '10'
:prop="expr" PHP expression :count="10" passes integer 10
prop Boolean true active passes true

Directive vs Tag Comparison

Both syntaxes are equivalent. The tag compiler converts <livue:*> tags into @livue() calls before Blade compilation.

Directive syntax
@livue('counter', ['count' => 5])
Tag syntax
<livue:counter :count="5" />

Special Options

The tag syntax supports special attributes for advanced component features:

<!-- Component ref for parent access -->
<livue:child ref="myChild" />

<!-- Two-way binding with #[Modelable] -->
<livue:search-input :value="$query" model="search" />

@script Setup

The @script / @endscript directives let you write Vue Composition API logic directly inside your Blade templates. The code runs in the setup() function of the dynamic Vue component, with full access to reactive state refs, the livue helper object, and standard Vue composables.

resources/views/livue/greeting.blade.php
<div>
    <p>Name: <strong v-text="name">{{ $name }}</strong></p>
    <p>Greeting: <strong v-text="greeting"></strong></p>
    <p>Local count: <strong v-text="localCount"></strong></p>

    <button @click="incrementLocal()">+1 Local</button>
    <button @click="increment()">+1 Server</button>
</div>

@script
const greeting = computed(() => 'Hello, ' + name.value + '!');
const localCount = ref(0);

function incrementLocal() {
    localCount.value++;
}

watch(name, (newVal) => {
    console.log('Name changed to:', newVal);
});

onMounted(() => {
    console.log('Component mounted!');
});

return { greeting, localCount, incrementLocal };
@endscript

Variables and functions returned from the @script block become available in the template alongside your server state. In the example above, greeting, localCount, and incrementLocal are all usable via Vue directives.

Available APIs

Inside @script, you have access to Vue Composition APIs and your component state without any imports:

Vue Composition API
  • ref() - Create reactive references
  • computed() - Computed properties
  • watch() - Watch changes
  • watchEffect() - Reactive effect
  • reactive() - Reactive objects
  • onMounted() - Mount lifecycle
  • onUnmounted() - Unmount lifecycle
  • nextTick() - DOM update timing
Component Access
  • name, count, etc. - PHP properties as refs
  • save(), increment(), etc. - Server methods (direct calls)
  • livue - The LiVue helper object
  • livue.toggle() - Toggle boolean properties
  • store(), useStore(), livue.useGlobalStore() - Pinia helpers
  • $errors - Validation errors

Quick Store Pattern in @script

Declare stores in PHP, then consume them from @script: use useStore() for component stores and LiVue::createStore() for global stores.

@script
const componentStore = useStore('demo-counter');
const globalStore = livue.useGlobalStore('demo-global-counter');
const appGlobalStore = livue.useGlobalStore('demo-app-counter');

componentStore.count++;
globalStore.count++;
appGlobalStore.count++;

const syncToBackend = () => livue.sync();

return { componentStore, globalStore, appGlobalStore, syncToBackend };
@endscript

Global stores created in AppServiceProvider with LiVue::createStore() are available everywhere via livue.useGlobalStore(name).

Rules

  • 1. Place @script outside the root <div>, at the end of your template
  • 2. Return an object with the bindings you want to expose to the template
  • 3. Only one @script block per template
  • 4. Local refs created in @script reset on server re-render. Use Pinia helpers (store(...), useStore(...)) when state must persist on the client.

IDE Support

For syntax highlighting and autocompletion, you can optionally wrap your code in a <script> tag. LiVue strips it automatically:

@script
<script>
// Now your IDE recognizes this as JavaScript!
const greeting = computed(() => 'Hello, ' + name.value);
const localCount = ref(0);

return { greeting, localCount };
</script>
@endscript

Vue Composition API in Blade

Inside @script, all your PHP public properties are available as Vue refs. Combined with the livue helper, you can build powerful interactions that blend server state with client-side logic.

Server State as Refs

Every public property on your PHP component is injected as a Vue ref. Access the value with .value in JavaScript:

@script
// 'name' and 'count' are PHP public properties, available as refs
const greeting = computed(() => 'Hello, ' + name.value + '!');
const doubled = computed(() => count.value * 2);

// Watch server state changes
watch(count, (newVal, oldVal) => {
    console.log(`Count changed: ${oldVal} → ${newVal}`);
});

return { greeting, doubled };
@endscript

The livue Helper

The livue object provides methods for interacting with the server and accessing component metadata:

API Description
method(...args) Call a server-side PHP method directly
livue.toggle('boolProperty') Toggle a boolean property on the server
livue.watch('prop', callback) Watch a server property for changes
livue.$el Access the component's root DOM element
livue.errors Reactive object containing validation errors (template shorthand: $errors)

Practical Example

Here is a complete example combining server state, client-side logic, lifecycle hooks, and the livue helper:

app/LiVue/ScriptTest.php
class ScriptTest extends Component
{
    public string $name = 'World';
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
}
resources/views/livue/script-test.blade.php
<div>
    <p>Count: <strong v-text="count">{{ $count }}</strong></p>
    <p>Double: <strong v-text="doubleCount"></strong></p>
    <p>Local: <strong v-text="localCount"></strong></p>

    <input type="text" v-model="name" />
    <button @click="increment()">+1 Server</button>
    <button @click="incrementLocal()">+1 Local</button>
</div>

@script
const doubleCount = computed(() => count.value * 2);
const localCount = ref(0);

function incrementLocal() {
    localCount.value++;
}

watch(name, (newVal) => {
    console.log('Name changed to:', newVal);
});

onMounted(() => {
    livue.call('loadData');
});

return { doubleCount, localCount, incrementLocal };
@endscript

Tip — Server state (PHP public properties) persists across template swaps because it lives in the LiVue state layer. Local state created with ref() inside @script resets after each server re-render because the Vue component is recreated.

Teleport

The @teleport / @endteleport directives let you render content outside of the component's DOM tree, using Vue 3's built-in <Teleport> component under the hood. This is particularly useful for modals, tooltips, and notifications that need to escape parent container styles.

Basic Syntax

Pass any valid CSS selector as the teleport target:

<!-- Teleport to the document body -->
@teleport('body')
    <div class="modal">...</div>
@endteleport

<!-- Teleport to a specific element -->
@teleport('#modal-container')
    <div class="dialog">...</div>
@endteleport

<!-- Teleport to an element by class -->
@teleport('.notifications')
    <div class="toast">...</div>
@endteleport

Modal Dialog Example

A common use case is rendering a modal dialog at the document body to avoid z-index and overflow issues:

resources/views/livue/confirm-dialog.blade.php
<div>
    <button @click="showModal = true">Open Modal</button>

    @teleport('body')
        <div v-if="showModal"
             class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
            <div class="bg-white p-6 rounded-lg shadow-xl max-w-md">
                <h2 class="text-xl font-bold mb-4">Confirm Action</h2>
                <p class="text-gray-600 mb-6">Are you sure you want to proceed?</p>
                <div class="flex gap-3 justify-end">
                    <button @click="showModal = false">Cancel</button>
                    <button @click="confirm()">Confirm</button>
                </div>
            </div>
        </div>
    @endteleport
</div>

@script
const showModal = ref(false);

return { showModal };
@endscript

Why Teleport?

  • 1. Z-index management — Teleported content avoids z-index stacking issues from parent containers
  • 2. CSS isolation — Styles like overflow: hidden on parent containers do not affect teleported content
  • 3. Clean DOM structure — Keep modals and overlays at the document root for better accessibility
  • 4. Full reactivity — Teleported content maintains full reactivity with the component state, methods, and computed properties

How It Works

The @teleport directive compiles to Vue's native <Teleport> component:

Blade (what you write)
@teleport('body')
    <div>Content</div>
@endteleport
Vue (compiled output)
<Teleport to="body">
    <div>Content</div>
</Teleport>

Tip — The teleport target element must exist in the DOM when the component mounts. You can use v-if inside @teleport for conditional rendering while keeping the teleport wrapper always present.