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

Features

Built-in features that handle common tasks: file uploads and downloads, real-time streaming, SPA navigation, confirm dialogs, loading indicators, tab synchronization, benchmarking, pagination and reusable PHP composables.

File Uploads

LiVue provides a complete file upload system with temporary storage, progress tracking, image previews and validation. Files are uploaded via a dedicated endpoint, stored temporarily on the server, and then your component decides where to persist them.

Enabling Uploads

Add the WithFileUploads trait to your component. Properties are detected automatically by their type hint:

app/LiVue/PhotoUploader.php
<?php

namespace App\LiVue;

use LiVue\Component;
use LiVue\Features\SupportFileUploads\WithFileUploads;
use LiVue\Features\SupportFileUploads\TemporaryUploadedFile;

class PhotoUploader extends Component
{
    use WithFileUploads;

    public ?TemporaryUploadedFile $photo = null;

    public array $documents = [];

    public function fileRules(): array
    {
        return [
            'photo' => ['image', 'max:2048'],       // 2MB, images only
            'documents' => ['file', 'max:5120'],     // 5MB, any file
        ];
    }

    public function savePhoto(): void
    {
        // Store with auto-generated name
        $path = $this->photo->store('photos');

        // Or store with a specific name
        // $path = $this->photo->storeAs('photos', 'avatar.jpg');

        $this->photo = null;
    }
}

Template

Use livue.upload() to handle the file input. After upload, the property contains metadata for display including a temporary preview URL for images:

resources/views/livue/photo-uploader.blade.php
<div>
    <!-- File input -->
    <input type="file" accept="image/*"
           @change="livue.upload('photo', $event.target.files[0])">

    <!-- Progress bar -->
    <div v-if="livue.uploading">
        Uploading... {{ livue.uploadProgress }}%
        <div :style="{ width: livue.uploadProgress + '%' }"></div>
    </div>

    <!-- Preview after upload -->
    <div v-if="photo">
        <img v-if="photo.previewUrl" :src="photo.previewUrl">
        <span v-text="photo.originalName"></span>
        <button @click="livue.removeUpload('photo')">Remove</button>
    </div>

    <!-- Errors -->
    <span v-if="$errors.photo" v-text="$errors.photo"></span>

    <button @click="savePhoto()">Save</button>
</div>

Multiple File Uploads

For array properties, use livue.uploadMultiple(). All files are sent in a single HTTP request. Valid files are accepted even if others fail validation:

Template
<input type="file" multiple
       @change="livue.uploadMultiple('documents', $event.target.files)">

<div v-for="(doc, index) in documents" :key="index">
    {{ doc.originalName }} - {{ Math.round(doc.size / 1024) }} KB
    <button @click="livue.removeUpload('documents', index)">Remove</button>
</div>

TemporaryUploadedFile Methods

After a successful upload, the PHP property holds a TemporaryUploadedFile with these methods:

Method Description
store($dir, $disk) Store with a random filename. Returns the path.
storeAs($dir, $name, $disk) Store with a specific filename. Returns the path.
temporaryUrl() Returns a signed URL for image preview (expires in 30 min).
getOriginalName() Returns the original file name.
getMimeType() Returns the MIME type.
getSize() Returns the file size in bytes.
delete() Deletes the temporary file.

Tip — Validation rules defined in fileRules() are applied at upload time, before the file is accepted. This means invalid files are rejected immediately, not when you call validate().

File Downloads

Trigger file downloads from server-side actions. LiVue provides two methods: one for existing files on disk and another for dynamically generated content.

download()

Download a file from the filesystem or a storage disk:

Component
public function downloadReport(): void
{
    // From local filesystem
    $this->download(storage_path('reports/monthly.pdf'), 'report.pdf');
}

public function downloadInvoice(int $id): void
{
    $invoice = Invoice::findOrFail($id);

    // From a storage disk (e.g. S3)
    $this->download(
        path: $invoice->file_path,
        name: "invoice-{$invoice->number}.pdf",
        disk: 's3'
    );
}

downloadContent()

Generate content on the fly and trigger a download. This is useful for exporting data as CSV, JSON or any other format:

CsvExporter.php
public function exportCsv(): void
{
    $users = User::all();

    $csv = "Name,Email\n";
    foreach ($users as $user) {
        $csv .= "{$user->name},{$user->email}\n";
    }

    $this->downloadContent($csv, 'users.csv', [
        'Content-Type' => 'text/csv',
    ]);
}

Template

Downloads are triggered from the template like any other action. The file is downloaded in the background without blocking the UI:

Template
<button @click="exportCsv()" :disabled="livue.loading">
    <span v-if="livue.isLoading('exportCsv')">Generating...</span>
    <span v-else>Export CSV</span>
</button>

Tip — Under the hood, the server creates an encrypted token containing the file path or content. The JavaScript runtime then fetches the file via a separate GET /livue/download endpoint. The token expires after 5 minutes.

Streaming

Stream content from the server to the client in real-time during long-running operations. This uses HTTP streaming (not WebSockets) via NDJSON, making it ideal for AI text generation, live logs, progress updates and more.

Enabling Streaming

Add the WithStreaming trait and use the stream() method to send data chunks to the client:

app/LiVue/AiChat.php
<?php

namespace App\LiVue;

use LiVue\Component;
use LiVue\Features\SupportStreaming\WithStreaming;

class AiChat extends Component
{
    use WithStreaming;

    public string $response = '';

    public function ask(string $question): void
    {
        $stream = $this->openai->chat($question, stream: true);

        foreach ($stream as $chunk) {
            $this->stream(to: 'output', content: $chunk);
        }

        $this->response = $stream->getFullResponse();
    }
}

The stream() Method

Each call sends a chunk to a named target in the template. By default, content is appended. Use replace: true to overwrite the previous content instead:

Component
// Append mode (default) - content accumulates
$this->stream(to: 'output', content: 'Hello ');
$this->stream(to: 'output', content: 'World!');
// Result: "Hello World!"

// Replace mode - content is overwritten
$this->stream(to: 'progress', content: '50%', replace: true);
$this->stream(to: 'progress', content: '100%', replace: true);
// Result: "100%"

v-stream Directive

Mark elements as stream targets with the v-stream directive. Use livue.stream() instead of a regular method call to invoke the streaming method:

Template
<!-- Append mode (default) -->
<div v-stream="'output'">Waiting for response...</div>

<!-- Replace mode -->
<div v-stream.replace="'status'">Ready</div>

<!-- Blinking cursor while streaming -->
<span v-if="livue.streaming" class="animate-pulse">|</span>

<button
    @click="livue.stream('ask', ['What is Vue?'])"
    :disabled="livue.streaming"
>
    <span v-if="livue.streaming">Generating...</span>
    <span v-else>Ask AI</span>
</button>

Tip — Streaming requests are automatically isolated from the request pooling system. The component state is updated normally when the stream finishes. You can also stream to multiple targets simultaneously.

LiVue supports client-side navigation that fetches pages via AJAX and swaps the body content without a full page reload. This provides an SPA-like experience with prefetching, scroll restoration and persistent components.

v-navigate Directive

Add v-navigate to any link to enable SPA navigation. Links are prefetched automatically:

Template
<!-- Prefetch on mousedown (default) -->
<a href="/dashboard" v-navigate>Dashboard</a>

<!-- Prefetch on hover (more aggressive, after 60ms delay) -->
<a href="/posts" v-navigate.hover>Posts</a>

<!-- Programmatic navigation -->
<button @click="livue.navigate('/settings')">Settings</button>

Server-side Redirect

Components can trigger navigation from the server. Use the navigate parameter to choose between a full page reload and an SPA transition:

Component
public function save(): void
{
    $this->validate();
    $this->model->save();

    // SPA navigation (no full reload)
    $this->redirect('/items', navigate: true);
}

public function cancel(): void
{
    // Classic redirect (full page reload)
    $this->redirect('/');
}

@persist Directive

Preserve DOM elements and their state across SPA page transitions. This is useful for video players, chat widgets, sidebars and LiVue components that should maintain their internal state:

layout.blade.php
<!-- Video keeps playing across pages -->
@persist('player')
    <video autoplay controls>
        <source src="...">
    </video>
@endpersist

<!-- LiVue component preserves its Vue state -->
@persist('sidebar')
    @livue('docs-sidebar')
@endpersist

Prefetching Strategies

Strategy When Use case
v-navigate Between mousedown and click (~100ms) Default, balanced
v-navigate.hover After 60ms of hover Faster navigation, more requests

Tip — Pages visited during SPA navigation are cached in memory for instant back/forward. Scroll positions are saved and restored automatically. External links and modifier-key clicks (Cmd/Ctrl) bypass SPA navigation.

Confirm Dialogs

Add confirmation prompts before executing destructive or important actions. By default, LiVue uses the browser's native confirm() dialog, but you can plug in any custom modal library.

#[Confirm] Attribute

Add the #[Confirm] attribute to any method. When the method is called from the template, the user is prompted before the AJAX request is sent:

app/LiVue/TaskManager.php
use LiVue\Attributes\Confirm;

class TaskManager extends Component
{
    // Simple message
    #[Confirm('Are you sure you want to delete this task?')]
    public function delete(int $id): void
    {
        Task::destroy($id);
    }

    // With title and custom button text
    #[Confirm(
        message: 'This action cannot be undone.',
        title: 'Delete All Tasks',
        confirmText: 'Yes, delete all',
        cancelText: 'No, keep them'
    )]
    public function deleteAll(): void
    {
        Task::truncate();
    }
}

Custom Confirm Handler

Replace the default browser dialog with your own UI library such as SweetAlert2, a Vue modal or any promise-based confirmation:

resources/js/app.js
import LiVue from 'livue';
import Swal from 'sweetalert2';

LiVue.setConfirmHandler(async (config) => {
    // config = { message, title, confirmText, cancelText }
    const result = await Swal.fire({
        title: config.title || 'Confirm',
        text: config.message,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonText: config.confirmText || 'Confirm',
        cancelButtonText: config.cancelText || 'Cancel',
    });

    return result.isConfirmed;
});

LiVue.start();

Tip — The confirm check runs entirely on the client side. If the user cancels, no AJAX request is sent. Loading indicators only appear after the user confirms.

Loading States

Track whether actions are in progress and provide visual feedback. LiVue offers both a JavaScript API and dedicated directives for managing loading states.

JavaScript API

The livue helper provides reactive properties and methods for loading state:

API Description
livue.loading true when any action is in progress
livue.isLoading() Returns true during any AJAX request
livue.isLoading('save') Returns true only while the save method is running
livue.processing Name of the method currently executing
livue.loadingAttrs('save') Returns { disabled, 'aria-busy' } bindings
Template
<!-- Per-action loading -->
<button @click="save()" :disabled="livue.isLoading('save')">
    <span v-if="livue.isLoading('save')">Saving...</span>
    <span v-else>Save</span>
</button>

<!-- Using loadingAttrs for accessible bindings -->
<button @click="submit()" v-bind="livue.loadingAttrs('submit')">
    Submit
</button>

v-loading Directive

The v-loading directive provides a declarative way to show, hide or style elements based on loading state:

Template
<!-- Show only when loading -->
<div v-loading>Loading...</div>

<!-- Show only when specific action loads -->
<div v-loading="'save'">Saving...</div>

<!-- Hide content during loading -->
<div v-loading.remove>Content hidden while loading</div>

<!-- Add CSS class during loading -->
<div v-loading.class="'opacity-50 pointer-events-none'">Fades when loading</div>

<!-- Add disabled attribute -->
<button v-loading.attr="'disabled'">Submit</button>

<!-- Delay to prevent flash on fast requests -->
<div v-loading.delay.short>Shows after 150ms</div>

v-target Directive

The v-target directive adds a data-loading attribute during loading, perfect for Tailwind's data-[loading]: variant:

Template
<button
    @click="delete()"
    v-target="'delete'"
    class="bg-red-500 text-white
           data-[loading]:opacity-50
           data-[loading]:cursor-wait"
>
    Delete
</button>

Progress Bar

LiVue includes a built-in progress bar (similar to NProgress) that shows during SPA navigation. By default, the progress bar is hidden during AJAX requests (component updates). You can enable it via configuration if desired.

Enabling on AJAX Requests

To show the progress bar during component update requests, enable it in your config:

config/livue.php
'progress' => [
    'show_on_request' => true,
],

Appearance Configuration

Customize the appearance and behavior via the JavaScript API:

resources/js/app.js
LiVue.progress.configure({
    color: '#3b82f6',      // Bar color (default: '#29d')
    height: '3px',         // Bar height (default: '2px')
    speed: 300,            // Animation speed in ms (default: 200)
    trickle: true,         // Enable trickle animation (default: true)
    trickleSpeed: 300,     // Trickle speed in ms (default: 200)
});

Manual Control

For custom async operations outside of LiVue, you can control the progress bar programmatically:

JavaScript
// Start the progress bar
LiVue.progress.start();

// Set a specific value (0 to 1)
LiVue.progress.set(0.5);

// Complete the bar
LiVue.progress.done();

// Check if active
LiVue.progress.isStarted();

Disabling on Navigation

The progress bar is already hidden by default on AJAX requests. To also disable it during SPA navigation:

config/livue.php
'navigate' => [
    'show_progress_bar' => false,
],

Tip — When enabled, the progress bar integrates with the request pooling system. It stays active across concurrent requests and only completes when all pending requests have resolved.

Tab Sync

Synchronize component state across browser tabs automatically. When a component is modified in one tab, all other tabs with the same component receive the update in real-time using the BroadcastChannel API.

#[TabSync] Attribute

Add the #[TabSync] attribute to your component class. Changes made via server actions in one tab will automatically appear in all other open tabs:

app/LiVue/ShoppingCart.php
use LiVue\Attributes\TabSync;

#[TabSync]
class ShoppingCart extends Component
{
    public array $items = [];
    public float $total = 0;

    public function addItem(array $item): void
    {
        $this->items[] = $item;
        $this->calculateTotal();
    }
}

Selective Sync

Control which properties are synchronized using only or except:

Component
// Only sync specific properties
#[TabSync(only: ['items', 'total'])]
class ShoppingCart extends Component
{
    public array $items = [];      // Synced
    public float $total = 0;       // Synced
    public string $coupon = '';   // NOT synced
}

// Exclude specific properties
#[TabSync(except: ['searchQuery'])]
class ProductList extends Component
{
    public array $products = [];       // Synced
    public string $searchQuery = '';  // NOT synced
}

Reactive Mode

By default, synced properties update only the Vue state. If your template uses Blade-rendered values ({{ $count }}), enable reactive mode to trigger a server re-render:

Component
// All properties trigger server refresh
#[TabSync(reactive: true)]
class Counter extends Component { ... }

// Only specific properties trigger server refresh
#[TabSync(reactive: ['count', 'items'])]
class Dashboard extends Component
{
    public int $count = 0;        // Triggers server refresh
    public string $filter = '';   // Vue-only update (no refresh)
}

Tip — Tab sync uses the BroadcastChannel API with a localStorage fallback for older browsers. Only changes from server actions are broadcast; local v-model changes are not synced unless they accompany a server action. This is ideal for shopping carts, notification counts and theme preferences.

Benchmarking

LiVue provides tools to measure the performance of every phase in the component lifecycle. You can profile components from the CLI with detailed per-phase breakdowns, or enable inline response timing to inspect server-side performance directly in the browser DevTools.

CLI Command

The livue:benchmark Artisan command profiles a component's mount and update lifecycle across multiple iterations and reports min, max, average and median timing for each phase, plus peak memory usage.

Terminal
# Benchmark mount lifecycle (10 iterations by default)
php artisan livue:benchmark Counter

# Benchmark a specific method call (update lifecycle)
php artisan livue:benchmark Counter --method=increment

# More iterations for stable results
php artisan livue:benchmark Counter --iterations=50

# JSON output for CI or tooling
php artisan livue:benchmark Counter --format=json

# Pass mount parameters
php artisan livue:benchmark Counter --mount-params='{"start":10}'

# Pass method parameters
php artisan livue:benchmark Counter --method=increment --params='[5]'

# Pass context variables (JSON object)
php artisan livue:benchmark Counter --context='{"tenant_id": 1, "locale": "en"}'

# Use a custom setup class for environment configuration
php artisan livue:benchmark ListProducts --setup="App\Support\TenantSetup" --context='{"tenant_id": 1}'

Options

Option Description
component Component name or fully-qualified class name (required)
--iterations Number of iterations to run (default: 10)
--method Method to call during update lifecycle benchmarking
--params JSON array of parameters for the method
--mount-params JSON object of parameters for mount
--format Output format: table (default) or json
--context JSON object with context variables for environment setup
--setup Fully qualified class implementing BenchmarkSetup for custom setUp/tearDown

Measured Phases

The command measures every internal lifecycle phase. During mount: boot, mount, forms, render, snapshot and total. During update: boot, hydrate_state, baseline_render, apply_diffs, distribute_memo, hydrate_hooks, method_call, dehydrate, events_flush, build_snapshot, render, collect_memo and total.

Results are color-coded with differentiated thresholds. Framework phases (boot, hydrate, dehydrate, etc.) use stricter limits: green <5ms, yellow 5-20ms, red >20ms. User phases (mount, method_call, render, total) use relaxed limits: green <50ms, yellow 50-200ms, red >200ms.

Environment Setup

The benchmark command instantiates components with new $class() outside any HTTP context — no request, session, or middleware. Components that depend on application state (e.g. a tenant context, authenticated user, or service singletons initialized by middleware) will fail without explicit setup.

LiVue provides two complementary mechanisms to configure the environment before benchmarking.

BenchmarkSetup Interface

Create a class implementing LiVue\Console\Contracts\BenchmarkSetup to run setup logic before the benchmark and cleanup after.

app/Support/TenantSetup.php
use LiVue\Console\Contracts\BenchmarkSetup;

class TenantSetup implements BenchmarkSetup
{
    public function setUp(array $context): void
    {
        // Initialize whatever your components need
        if (isset($context['tenant_id'])) {
            Tenancy::initialize($context['tenant_id']);
        }
    }

    public function tearDown(): void
    {
        Tenancy::end();
    }
}
Terminal
php artisan livue:benchmark ListProducts --setup="App\Support\TenantSetup" --context='{"tenant_id": 1}'

The setUp() method receives the parsed --context array. The tearDown() method runs in a finally block, so it executes even if the benchmark throws an exception.

Automatic Setup via Events

For packages that need to configure the environment automatically (without requiring the user to pass --setup), LiVue dispatches two Laravel events around the benchmark:

Event When Properties
BenchmarkStarting Before iterations begin componentClass, context, method, iterations
BenchmarkFinished After iterations complete componentClass, context, mountResults, updateResults
ServiceProvider
use LiVue\Events\BenchmarkStarting;
use LiVue\Events\BenchmarkFinished;

// In your ServiceProvider boot()
Event::listen(BenchmarkStarting::class, function (BenchmarkStarting $event) {
    if (isset($event->context['tenant_id'])) {
        Tenancy::initialize($event->context['tenant_id']);
    }
});

Event::listen(BenchmarkFinished::class, function () {
    Tenancy::end();
});

Tip — Both mechanisms work together. Use --setup for one-off setup needs and event listeners for automatic setup that activates whenever a package is installed.

Inline Response Timing

Enable per-phase timing data in every AJAX response. This adds a benchmark key to the JSON response with timing for each lifecycle phase in microseconds.

config/livue.php
'benchmark_responses' => env('LIVUE_BENCHMARK', false),
.env
LIVUE_BENCHMARK=true

When enabled, the benchmark data is visible in the DevTools Performance tab under the "Server Lifecycle Timing" section, giving you a per-phase breakdown of every server round-trip.

Tip — Benchmarking is only available in local/development environments and is off by default. There is no performance overhead in production. Enable it via LIVUE_BENCHMARK=true in your .env file when you need it.

PHP Composables

PHP Composables are reusable units of server-side logic that expose data and actions directly to your Vue templates under clean namespaces. Create a trait with a use* method, add it to your component, and access everything via namespace.property or namespace.action() in the template.

app/LiVue/Dashboard.php
class Dashboard extends Component
{
    use UseAuth, UseCart;
}

// Template: auth.name, auth.logout(), cart.items, cart.add(42)

Composables support auto-discovery, private persistent state, global registration from service providers, and more.

Read the full Composables guide →