Livewire UI Components - v2.0.0
Livewire 4 Features (v2.0)
Version 2.0 of ArtisanPack UI Livewire Components introduces support for several Livewire 4-specific features. These features are designed to work alongside existing Livewire 3 functionality, providing enhanced capabilities for users on Livewire 4 while maintaining full backward compatibility.
Overview
The following Livewire 4 features are now supported:
| Feature | Description | Components |
|---|---|---|
#[Computed] |
Attribute-based computed properties | Calendar |
#[Lazy] |
Deferred component loading with placeholders | Calendar |
#[Renderless] |
Skip re-render for non-UI operations | Custom components |
| Islands | Isolated component regions for partial updates | Calendar |
wire:stream |
Real-time content streaming for AI integration | StreamableContent |
wire:intersect |
Infinite scroll and lazy loading | Table, Choices |
data-loading |
CSS-based loading states | Button, MenuItem, Header, Card, Separator, Tags, Choices |
wire:sort |
Native drag-and-drop sorting | Table |
Backward Compatibility
All Livewire 4 features are implemented with full backward compatibility:
- Livewire 3 users: Features are gracefully ignored or work through existing
wire:loadingdirectives - Livewire 4 users: Get enhanced functionality through new directives and CSS variants
- No breaking changes: Existing code continues to work without modification
#[Computed] Attribute
The #[Computed] attribute provides a cleaner syntax for defining computed properties in Livewire components. This attribute is available in both Livewire 3 and Livewire 4, making it fully backward compatible.
Migration from Legacy Syntax
Before (Legacy getter syntax):
public function getWeekdaysProperty(): Collection
{
return collect(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
}
public function getWeeksProperty(): Collection
{
// Complex calculation...
return $weeks;
}
After (Attribute syntax):
use Livewire\Attributes\Computed;
#[Computed]
public function weekdays(): Collection
{
return collect(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
}
#[Computed(persist: true, seconds: 60)]
public function weeks(): Collection
{
// Complex calculation...
return $weeks;
}
Caching with #[Computed]
The #[Computed] attribute supports optional caching parameters:
| Parameter | Type | Description |
|---|---|---|
persist |
bool | Whether to cache the computed value across requests |
seconds |
int | How long to cache the value in seconds |
// No caching - recalculated on each access
#[Computed]
public function simpleData(): array { }
// Cached for 60 seconds across requests
#[Computed(persist: true, seconds: 60)]
public function expensiveData(): array { }
Components Using #[Computed]
Calendar Component
The Calendar component uses #[Computed] for its weekdays and weeks calculations:
use Livewire\Attributes\Computed;
#[Computed]
public function weekdays(): Collection
{
// Returns weekday names based on locale and start day
}
#[Computed]
public function weeks(): Collection
{
// Returns the calendar grid with events
// Recalculated per-request since it depends on dynamic state
// (gridStartsAt, events) that changes during navigation
}
Accessing Computed Properties
Computed properties are accessed the same way regardless of which syntax you use:
{{-- In Blade templates --}}
@foreach ($this->weekdays as $weekday)
{{ $weekday->format('l') }}
@endforeach
@foreach ($this->weeks as $week)
{{-- Render week --}}
@endforeach
Feature Detection
You can check if the #[Computed] attribute is available:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
if (LivewireHelper::supportsComputedAttribute()) {
// #[Computed] attribute is available
}
#[Lazy] Attribute - Deferred Loading
The #[Lazy] attribute enables deferred loading for heavy Livewire components. When applied, the component displays a placeholder until it becomes visible in the viewport, then hydrates and renders the full component. This is available in both Livewire 3 and Livewire 4.
How It Works
- The component is marked with
#[Lazy]attribute - Initially, only a lightweight placeholder is rendered
- When the placeholder enters the viewport (via Intersection Observer), the full component loads
- The
placeholder()method defines what users see while loading
Components Using #[Lazy]
Calendar Component
The Calendar component uses #[Lazy] for deferred loading since it performs complex calculations:
use Livewire\Attributes\Lazy;
#[Lazy]
class Calendar extends Component
{
public function placeholder(array $params = []): View
{
return view('livewire-ui-components::placeholders.calendar', $params);
}
}
Placeholder Views
Components with #[Lazy] provide skeleton UI placeholders that match their structure:
{{-- resources/views/placeholders/calendar.blade.php --}}
<div class="w-full animate-pulse">
<div class="bg-white dark:bg-base-200 border rounded-lg p-4">
{{-- Header skeleton --}}
<div class="mb-4 flex items-center justify-between">
<div class="w-32 h-8 bg-gray-200 rounded"></div>
<div class="w-40 h-6 bg-gray-200 rounded"></div>
</div>
{{-- Grid skeleton --}}
<div class="grid grid-cols-7 gap-2">
@for ($i = 0; $i < 35; $i++)
<div class="h-20 bg-gray-100 rounded"></div>
@endfor
</div>
</div>
</div>
Usage
The #[Lazy] attribute is applied at the class level:
{{-- The Calendar component loads lazily by default --}}
<livewire:calendar :events="$events" />
{{-- You can disable lazy loading if needed --}}
<livewire:calendar :events="$events" lazy="false" />
Benefits
- Improved Initial Page Load: Heavy components don't block initial render
- Reduced Server Load: Components below the fold don't hydrate until needed
- Better UX: Users see a skeleton placeholder instead of a blank space
- Automatic: Works transparently with existing component usage
Feature Detection
You can check if the #[Lazy] attribute is available:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
if (LivewireHelper::supportsLazyAttribute()) {
// #[Lazy] attribute is available
}
#[Renderless] Attribute - Non-UI Operations
The #[Renderless] attribute allows Livewire methods to execute without triggering a component re-render. This is ideal for operations that don't change the UI, such as analytics tracking, view count increments, and logging.
How It Works
When a method is marked with #[Renderless]:
- The method executes on the server
- No re-render is triggered
- The component state remains unchanged on the client
- Network payload is minimized (no HTML returned)
Use Cases
The #[Renderless] attribute is perfect for:
- Analytics Tracking: Log user interactions without UI updates
- View Count Increments: Track page/content views silently
- Logging Operations: Record actions for debugging or auditing
- Background Notifications: Send notifications without affecting the UI
- Session Updates: Update session data silently
Implementation Example
use Livewire\Attributes\Renderless;
use Livewire\Component;
class ArticleView extends Component
{
public $article;
/**
* Track article view without re-rendering the component.
*/
#[Renderless]
public function trackView(): void
{
$this->article->increment('view_count');
// Log the view for analytics
Activity::log('article.viewed', [
'article_id' => $this->article->id,
'user_id' => auth()->id(),
]);
}
/**
* Track social share without re-rendering.
*/
#[Renderless]
public function trackShare(string $platform): void
{
Analytics::track('social_share', [
'platform' => $platform,
'article_id' => $this->article->id,
]);
}
public function render()
{
return view('livewire.article-view');
}
}
Usage in Blade
<div>
{{-- Track view on component load --}}
<div wire:init="trackView">
<h1>{{ $article->title }}</h1>
<p>{{ $article->content }}</p>
</div>
{{-- Track shares without UI update --}}
<div class="share-buttons">
<button wire:click="trackShare('twitter')">Share on Twitter</button>
<button wire:click="trackShare('facebook')">Share on Facebook</button>
</div>
</div>
Combining with Other Attributes
You can combine #[Renderless] with other attributes:
use Livewire\Attributes\Renderless;
use Livewire\Attributes\On;
class Dashboard extends Component
{
/**
* Log when a notification is received (event listener, no re-render).
*/
#[On('notification-received')]
#[Renderless]
public function logNotification(array $data): void
{
Log::info('Notification received', $data);
}
}
Benefits
- Performance: No unnecessary DOM updates
- Reduced Bandwidth: Server returns minimal response
- Clean Separation: UI logic stays separate from tracking/logging
- User Experience: No visual flicker for background operations
Feature Detection
You can check if the #[Renderless] attribute is available:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
if (LivewireHelper::supportsRenderlessAttribute()) {
// #[Renderless] attribute is available
}
Livewire 3 Fallback
In Livewire 3, if the #[Renderless] attribute is not available, the method will still execute but will trigger a re-render. To optimize for Livewire 3:
public function trackView(): void
{
$this->article->increment('view_count');
// In Livewire 3, use skipRender() as fallback
if (!LivewireHelper::supportsRenderlessAttribute()) {
$this->skipRender();
}
}
Islands Architecture - Isolated Updates
Islands architecture allows different parts of a complex component to update independently without affecting other regions. This is particularly useful for components like calendars, dashboards, and data-heavy interfaces where you want header updates to not re-render the entire grid.
How It Works
- Components are divided into named "islands" using
@islandand@endislanddirectives - Each island gets a unique
wire:keyfor Livewire's morphing algorithm - The directive outputs:
<div wire:key="island-{name}" data-island="{name}"> - This is compatible with both Livewire 3 and Livewire 4
Compatibility
| Version | Behavior |
|---|---|
| Livewire 4+ | Optimized partial updates per-island for better performance |
| Livewire 3 | Standard rendering with wire:key isolation (fully functional) |
The @island directive uses standard Livewire features (wire:key) that work in all versions. The performance optimization is automatic in Livewire 4+ due to its improved morphing algorithm.
Blade Directives
The package provides custom Blade directives for Islands:
| Directive | Description |
|---|---|
@island('name') |
Creates an isolated region with wire:key |
@island('name', ['defer' => true]) |
Creates a deferred region (Livewire 4+) |
@endisland |
Closes the island region |
@placeholder |
Shows placeholder content while deferred island loads |
@endplaceholder |
Closes the placeholder region |
Components Using Islands
Calendar Component
The Calendar component uses Islands to separate header and grid updates:
{{-- Header Island - navigation buttons and view selector --}}
@island('calendar-header')
<div class="flex items-center justify-between">
<button wire:click="goToPreviousPeriod">Previous</button>
<h2>{{ $headerText }}</h2>
<button wire:click="goToNextPeriod">Next</button>
</div>
@endisland
{{-- Grid Island - the calendar grid with events --}}
@island('calendar-grid')
<div class="grid grid-cols-7">
{{-- Calendar grid content --}}
</div>
@endisland
With this architecture:
- Clicking "Previous" or "Next" updates the header island independently
- The grid updates only when necessary (new month, events change)
- In Livewire 4+, the DOM morphing is optimized per-island
Deferred Islands
For heavy content regions, you can defer loading until the island is visible:
@island('heavy-content', ['defer' => true])
@placeholder
<div class="animate-pulse h-64 bg-gray-200 rounded"></div>
@endplaceholder
{{-- Heavy content that loads when visible --}}
<div class="chart-container">
@foreach($data as $item)
{{-- Complex rendering --}}
@endforeach
</div>
@endisland
In Livewire 4+:
- The placeholder shows initially
- When the island enters the viewport,
wire:intersect.oncetriggers a refresh - The full content loads and replaces the placeholder
In Livewire 3:
- The content renders immediately (no deferral)
- The placeholder is skipped
- Everything still works, just without the optimization
Custom Implementation
You can use Islands in your own Livewire components:
<div>
{{-- Sidebar island - filters and options --}}
@island('sidebar')
<aside class="sidebar">
<x-artisanpack-select wire:model.live="filter" :options="$filters" />
<x-artisanpack-checkbox wire:model.live="showArchived" label="Show Archived" />
</aside>
@endisland
{{-- Main content island - data display --}}
@island('main-content')
<main class="content">
@foreach($items as $item)
<x-artisanpack-card>{{ $item->name }}</x-artisanpack-card>
@endforeach
</main>
@endisland
{{-- Pagination island --}}
@island('pagination')
<x-artisanpack-pagination :rows="$items" />
@endisland
</div>
Benefits
- Performance: Reduced DOM updates when only part of a component changes
- User Experience: Faster perceived updates, no full component flicker
- Optimized Morphing: Livewire 4's morphing algorithm works per-island
- Backward Compatible: Falls back gracefully in Livewire 3
Feature Detection
You can check if Islands are fully supported:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
if (LivewireHelper::supportsIslands()) {
// Livewire 4+ with full Islands support
}
Best Practices
- Use meaningful island names: Names should describe the region's purpose (
calendar-header,user-list,sidebar-filters) - Don't over-partition: Too many small islands can negate the benefits; group related elements
- Defer wisely: Only defer heavy content that's below the fold or computationally expensive
- Test both versions: Ensure your component works correctly in both Livewire 3 and 4
wire:stream - Real-Time Content Streaming
Livewire 4's wire:stream directive enables real-time content streaming, particularly useful for AI-generated content that appears character by character or chunk by chunk. This is a Livewire 4 only feature.
How It Works
- An element is marked with
wire:stream="targetName" - Server-side code calls
$this->stream('targetName', $content)to send content - Content appears in real-time as it's streamed from the server
- The stream can append or replace content
StreamableContent Component
The package provides a StreamableContent component designed for streaming:
<x-artisanpack-streamable-content
target="aiResponse"
placeholder="Waiting for AI response..."
:show-cursor="true"
:prose="true"
/>
Component Properties
| Property | Type | Default | Description |
|---|---|---|---|
target |
string | (required) | The wire:stream target identifier |
id |
string | null | Custom element ID |
tag |
string | 'div' | HTML tag to use |
prose |
bool | false | Apply prose styling for rich text |
placeholder |
string | null | Placeholder text before content arrives |
show-cursor |
bool | false | Show blinking cursor during streaming |
Use Case: AI Writing Assistant
A common use case is integrating AI content generation with the Editor component:
use Livewire\Component;
class ArticleEditor extends Component
{
public string $content = '';
public string $aiSuggestion = '';
public function generateAIContent(string $prompt): void
{
// Stream AI response chunk by chunk
foreach ($this->aiService->streamResponse($prompt) as $chunk) {
$this->stream(
to: 'aiSuggestion',
content: $chunk,
replace: false, // Append to existing content
);
}
}
public function insertSuggestion(): void
{
$this->content .= $this->aiSuggestion;
$this->aiSuggestion = '';
}
}
<div>
{{-- Main Editor --}}
<x-artisanpack-editor wire:model="content" label="Article Content" />
{{-- AI Suggestion Panel --}}
<div class="mt-4 p-4 border rounded-lg bg-base-200">
<h3 class="font-semibold mb-2">AI Suggestion</h3>
<x-artisanpack-streamable-content
target="aiSuggestion"
placeholder="Click 'Generate' to get AI suggestions..."
:show-cursor="true"
:prose="true"
class="min-h-[100px] p-3 bg-base-100 rounded"
/>
<div class="mt-3 flex gap-2">
<x-artisanpack-button
wire:click="generateAIContent('Continue this article...')"
color="primary"
>
Generate
</x-artisanpack-button>
<x-artisanpack-button
wire:click="insertSuggestion"
color="secondary"
>
Insert into Editor
</x-artisanpack-button>
</div>
</div>
</div>
Custom Implementation
You can use wire:stream with any element:
{{-- Simple streaming text --}}
<div wire:stream="responseText">{{ $responseText }}</div>
{{-- With the StreamableContent component --}}
<x-artisanpack-streamable-content
target="chatResponse"
tag="p"
:show-cursor="true"
/>
Server-side streaming:
public function chat(string $message): void
{
$response = $this->openAI->chat($message);
// Stream each chunk as it arrives
foreach ($response->stream() as $chunk) {
$this->stream('chatResponse', $chunk->content);
}
// Mark streaming complete (removes cursor)
$this->dispatch('streaming-complete', target: 'chatResponse');
}
Streaming Options
The stream() method accepts these parameters:
$this->stream(
to: 'targetName', // Required: The wire:stream target
content: $text, // Required: Content to stream
replace: false, // Optional: Replace vs append (default: false = append)
);
Feature Detection
Check if streaming is supported before using it:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
if (LivewireHelper::supportsWireStream()) {
// Use streaming for real-time updates
$this->stream('output', $chunk);
} else {
// Livewire 3 fallback: accumulate and update at once
$this->output .= $chunk;
}
Livewire 3 Behavior
In Livewire 3, the StreamableContent component renders normally without the wire:stream attribute. Content must be sent all at once rather than streamed:
public function generateContent(string $prompt): void
{
if (LivewireHelper::supportsWireStream()) {
// Livewire 4+: Stream in real-time
foreach ($this->aiService->streamResponse($prompt) as $chunk) {
$this->stream('aiContent', $chunk);
}
} else {
// Livewire 3: Get full response and update property
$this->aiContent = $this->aiService->getFullResponse($prompt);
}
}
Best Practices
- Show loading state: Use a placeholder or loading indicator before streaming starts
- Provide visual feedback: The
show-cursoroption helps users know content is being streamed - Handle errors gracefully: Wrap streaming logic in try-catch and show error messages
- Consider rate limiting: AI services may have rate limits; handle them appropriately
- Test without streaming: Ensure your UI works when streaming isn't supported (Livewire 3)
data-loading Attribute System
Livewire 4 introduces a CSS-based loading state system using the data-loading attribute. Components now support both the traditional wire:loading directive and the new CSS variant classes.
How It Works
In Livewire 4, during a loading state, Livewire adds a data-loading attribute to elements. This enables CSS-based visibility control through Tailwind variants:
| CSS Class | Behavior |
|---|---|
data-loading:block |
Shows element during loading |
data-loading:hidden |
Hides element during loading |
not-data-loading:hidden |
Hides element when NOT loading |
Components with data-loading Support
Button Component
The Button component uses data-loading for spinner and loading content:
{{-- Spinner shows during loading --}}
<x-artisanpack-button spinner wire:click="save">
Save
</x-artisanpack-button>
{{-- Custom loading content --}}
<x-artisanpack-button spinner loading="Saving..." wire:click="save">
Save
</x-artisanpack-button>
Internal implementation (for reference):
<!-- Spinner: hidden by default, shown during loading -->
<span class="loading loading-spinner hidden data-loading:block"
wire:loading.class.remove="hidden">
</span>
<!-- Icon: shown by default, hidden during loading -->
<span class="data-loading:hidden"
wire:loading.class="hidden">
<x-artisanpack-icon name="o-check" />
</span>
MenuItem Component
Menu items with spinners use the same dual-support pattern:
<x-artisanpack-menu-item
title="Save"
icon="o-check"
spinner
wire:click="save" />
Header & Card Components
Progress indicators in Header and Card components support data-loading:
<x-artisanpack-header
title="Page Title"
separator
progress-indicator />
<x-artisanpack-card
title="Card Title"
separator
progress-indicator />
Separator Component
The Separator component's progress bar supports data-loading:
<x-artisanpack-separator progress wire:target="search" />
Tags & Choices Components
Search progress indicators support data-loading:
<x-artisanpack-tags
wire:model="tags"
searchable />
<x-artisanpack-choices
wire:model="selection"
:options="$options"
searchable />
Custom Implementation
If you're creating custom components, you can implement the same pattern:
{{-- Element that SHOWS during loading --}}
<div class="hidden data-loading:block"
wire:loading.class.remove="hidden"
wire:target="myAction">
Loading...
</div>
{{-- Element that HIDES during loading --}}
<div class="data-loading:hidden"
wire:loading.class="hidden"
wire:target="myAction">
Content
</div>
wire:intersect - Infinite Scroll
Livewire 4's wire:intersect directive enables intersection observer-based lazy loading. This is used for infinite scroll in the Table component and lazy loading options in the Choices component.
Table Component - Infinite Scroll
<x-artisanpack-table
:headers="$headers"
:rows="$users"
infinite-scroll
infinite-scroll-method="loadMore"
:has-more-pages="$hasMorePages" />
See the Table Component documentation for full details.
Choices Component - Lazy Loading
<x-artisanpack-choices
:options="$users"
wire:model="selectedUser"
lazy-load
lazy-load-method="loadMoreOptions"
:has-more-options="$hasMore" />
See the Choices Component documentation for full details.
Modifiers
Both components support Livewire 4's intersection modifiers:
| Modifier | Description |
|---|---|
.once |
Only trigger once |
.half |
Trigger when element is 50% visible |
.full |
Trigger when element is fully visible |
<x-artisanpack-table
:headers="$headers"
:rows="$users"
infinite-scroll
infinite-scroll-modifier="once"
:has-more-pages="$hasMorePages" />
wire:sort - Drag-and-Drop Sorting
Livewire 4's native wire:sort directive enables drag-and-drop row reordering in tables without JavaScript libraries.
Table Component - Sortable Rows
<x-artisanpack-table
:headers="$headers"
:rows="$items"
sortable
wire:sort="updateOrder" />
See the Table Component documentation for full details.
Feature Detection
The package includes a LivewireHelper utility class that detects Livewire version and feature support:
use ArtisanPack\LivewireUiComponents\Support\LivewireHelper;
// Check Livewire version
if (LivewireHelper::isLivewire4()) {
// Livewire 4+ specific code
}
// Check specific feature support
if (LivewireHelper::supportsComputedAttribute()) {
// #[Computed] attribute is available
}
if (LivewireHelper::supportsLazyAttribute()) {
// #[Lazy] attribute is available
}
if (LivewireHelper::supportsRenderlessAttribute()) {
// #[Renderless] attribute is available
}
if (LivewireHelper::supportsAsyncActions()) {
// Livewire 4+ native async support is available
}
if (LivewireHelper::supportsIslands()) {
// Livewire 4+ with full Islands support for isolated updates
}
if (LivewireHelper::supportsWireStream()) {
// Livewire 4+ with wire:stream for real-time content streaming
}
if (LivewireHelper::supportsWireIntersect()) {
// wire:intersect is available
}
if (LivewireHelper::supportsWireSort()) {
// wire:sort is available
}
Tailwind CSS Configuration
The data-loading CSS variants require Tailwind CSS 3.4+ with the variant plugin or custom configuration. The package includes a JIT safelist comment in components to ensure classes are generated.
If you're using a custom Tailwind configuration, ensure these variants are available:
// tailwind.config.js (if needed)
module.exports = {
plugins: [
// The data-* variants are built-in to Tailwind CSS 3.4+
// For older versions, you may need:
plugin(function({ addVariant }) {
addVariant('data-loading', '&[data-loading]')
addVariant('not-data-loading', '&:not([data-loading])')
})
]
}
Troubleshooting
data-loading classes not working
- Check Tailwind version: Ensure you're using Tailwind CSS 3.4+ which includes built-in
data-*variants - JIT mode: Ensure Tailwind JIT is scanning your component files
- Livewire version: The
data-loadingattribute is only added by Livewire 4+
wire:intersect not triggering
- Livewire version: Requires Livewire 4+
- Element visibility: Ensure the target element is visible in the viewport
- Method exists: Verify the method specified exists on your Livewire component
wire:sort not working
- Livewire version: Requires Livewire 4+
- Row keys: Ensure each row has a unique identifier
- Method signature: The sort method should accept an array of ordered IDs
#[Lazy] component not loading
- Placeholder view missing: Ensure the placeholder view exists at the specified path
- Intersection Observer: The component requires the placeholder to enter the viewport to trigger loading
- JavaScript disabled: Lazy loading requires JavaScript; the component will not load without it
- Livewire version: The
#[Lazy]attribute is available in both Livewire 3 and 4
#[Renderless] still re-rendering
- Attribute class missing: Verify
Livewire\Attributes\Renderlessexists in your Livewire installation - Method signature: Ensure the method has the
#[Renderless]attribute directly above the method - Livewire 3 fallback: Use
$this->skipRender()inside the method as a fallback for older versions - State changes: If the method modifies public properties, Livewire may still re-render; keep renderless methods state-free