Open source October 2026
Wirelist

Wirelist

Wirelist

Wirelist draws collections of records that are read one at a time rather than compared column by column: a feed of tickets, a page of episodes, a wall of property cards. It is three anonymous Blade components and plain CSS, with the row menu, the footer, load more and the themes of wiretables underneath.

A list or a table?

Both packages render records and are designed to share a screen.

  • A table (wiretables) when every record has the same fields and the reader scans down a column: due dates, amounts, statuses.
  • A list when each record is a small story of its own: a name, a line of context, a couple of tags, one or two numbers. No headers, because nothing is compared vertically, and records can sit side by side as cards.

A list paints with wiretables' tokens, so a table and a list on the same page share colours, radius and hover, and change together with the theme.

Installation

composer require edulazaro/wirelist

PHP 8.2+, Laravel 11+. wiretables comes with it. The service providers are auto discovered; there is no config and nothing to migrate.

The list reads its colours from wiretables, so both packages' CSS go on the page, wiretables first, core before theme. With Vite, at the very top of resources/css/app.css (postcss-import drops an @import placed after other rules):

@import "../../vendor/edulazaro/wiretables/resources/css/wiretables-core.css";
@import "../../vendor/edulazaro/wiretables/resources/css/themes/minimal.css";
@import "../../vendor/edulazaro/wirelist/resources/css/wirelist-core.css";
@import "../../vendor/edulazaro/wirelist/resources/css/themes/minimal.css";

Or publish them to public/vendor and link the same four files:

php artisan vendor:publish --tag=wiretables-assets
php artisan vendor:publish --tag=wirelist-assets

wirelist.css and wiretables.css are bundles with every theme, handy for trying them out. A static list needs no JavaScript; the row menu needs Alpine with its anchor plugin and load more needs Livewire (Livewire 3 and 4 bundle both).

A first list

<x-wirelist>
    @forelse ($episodes as $episode)
        <x-wirelist.item
            wire:key="episode-{{ $episode->id }}"
            :title="$episode->title"
            :subtitle="$episode->guest_name"
            :href="route('episodes.show', $episode)"
        >
            <x-slot:aside>
                <span>{{ $episode->duration_minutes }} min</span>
            </x-slot:aside>
        </x-wirelist.item>
    @empty
        <x-wirelist.empty>No episodes yet.</x-wirelist.empty>
    @endforelse
</x-wirelist>

One framed block with a line between episodes, the duration on the right, and a click anywhere on a row opens the episode. <x-wirelist.empty> is a <li>, so it goes inside the list and the frame stays in place.

Shapes

The items never change; the wrapper decides the shape.

Attribute Result
items="divided" Default. One frame, a hairline between records
items="separated" One card per record, with a gap between them
layout="grid" Cards in as many columns as fit (at least --wtl-grid-min wide each). Always separated
flush No side padding and no clipping, for a list inside a card that already has its own

In a grid, an <img> in the leading slot becomes an edge to edge cover, the aside drops to the foot of the card and the actions float in the top right corner. Since the markup is the same, a list/grid switch is one bound attribute:

<x-wirelist items="separated" :layout="$view">...</x-wirelist>

A footer slot on the list renders under the records: pagination links, a total, or load more.

What an item carries

Place Given as For
title attribute, or slot when it needs markup The record's name
subtitle attribute One muted line, skipped when empty
leading slot Avatar, icon, thumbnail; the cover in a grid
badges slot Labels beside the title (bring your own badge markup)
aside slot Short figures on the right: a count, a status, a date
actions slot The "⋯" menu, or a single button
header slot A strip across the top: "Overdue by 4 days"
default slot More lines under the subtitle: an excerpt, a progress bar

What a click does:

  • href: the title becomes a link stretched over the whole item. Add navigate for wire:navigate.
  • action: an Alpine expression, run by a stretched <button>, for opening a modal or a panel in place.
  • neither: a plain title and no hover, for read-only records.

The link is stretched rather than wrapped around the item because HTML does not allow buttons or links inside a link. Badges, aside, actions and any link, button, input, select or label in the content sit above it and keep their own clicks.

<x-wirelist.item
    wire:key="order-{{ $order->id }}"
    :title="'Order '.$order->reference"
    :subtitle="$order->customer_name"
    :href="route('orders.show', $order)"
    navigate
>
    <x-slot:leading><img src="{{ $order->thumbnail }}" alt="" width="48" height="48"></x-slot:leading>
    <x-slot:badges><span class="tag">Gift wrap</span></x-slot:badges>
    <x-slot:aside>
        <span>{{ $order->items_count }} items</span>
        <span>€{{ number_format($order->total, 2) }}</span>
    </x-slot:aside>
</x-wirelist.item>

Under 40rem wide, a list keeps the actions next to the title and moves the aside to a line of its own below.

The row menu

The menu is wiretables' <x-wiretable.menu>, placed in the actions slot. Its panel is teleported to <body> and anchored to the button, so a rounded frame or a scrolling card never crops it, and opening it never opens the record underneath.

<x-slot:actions>
    <x-wiretable.menu :label="'Actions for '.$article->headline">
        <x-wiretable.menu-item :href="route('articles.preview', $article)">Preview</x-wiretable.menu-item>
        <x-wiretable.menu-item wire:click="duplicate({{ $article->id }})">Duplicate</x-wiretable.menu-item>
        <x-wiretable.menu-separator />
        <x-wiretable.menu-item wire:click="unpublish({{ $article->id }})" danger>Unpublish</x-wiretable.menu-item>
    </x-wiretable.menu>
</x-slot:actions>

label is required: it is the icon button's accessible name, so name the record in it. A menu item is a link with href and a button otherwise, takes danger and a leading slot for an icon. The items are left aligned by default; align="right" moves their text to the right edge of the panel without moving the panel.

Load more

wiretables' WithLoadMore trait and its button work on a list unchanged:

use EduLazaro\Wiretables\Concerns\WithLoadMore;

class NewsroomFeed extends Component
{
    use WithLoadMore;

    public function render()
    {
        return view('livewire.newsroom-feed', [
            'articles' => $this->loadMoreFrom(Article::query()->latest('published_at')),
        ]);
    }
}
<x-slot:footer>
    <x-wiretable.load-more :show="$hasMore" />
</x-slot:footer>

It reads every page shown so far from the start (30 per load, override perLoad()), so a record published between clicks never shifts the feed. Keep the wire:key on each item. WithSorting also works, with your own buttons calling sortBy(); see wiretables.

Theming

Themes are chosen with data-wire-theme, the attribute shared with wiremodal, wiretoast, wirepicker and wiretables, usually on <html> but on any element for one section. Load the theme from both packages:

<html data-wire-theme="toxic">

Twelve themes: soft, glass, gradient, neon, minimal, claude, chatgpt, studio, synthwave, megaflow, brutalist and toxic (swamp dark with a lime accent; titles take the accent and a linked card lights its edge on hover). wiretables' theme file already colours the list; wirelist's adds only the list touches: stripes, glows, the header strip. Dark mode follows wiretables (data-wire-theme-mode="dark" or .dark); neon and toxic are always dark.

Colours come in layers: --wire-* for the whole family, --wtb-* from wiretables (what a list paints with), and --wtl-* for the list's own geometry.

[data-wire-theme="minimal"] {
    --wire-accent: #0f766e;   /* lists, tables, modals and toasts */
}

.photo-wall {
    --wtl-grid-min: 12rem;
    --wtl-grid-gap: 8px;
    --wtl-cover-ratio: 1;
}

Set --wtb-* overrides on the list or a container, not on :root: wiretables declares them again on every themed element.

Reference

Component Attribute Default
<x-wirelist> items divided divided or separated
layout list list or grid
flush false No side padding, no clipping
<x-wirelist.item> title null Escaped; a title slot replaces it
subtitle null Rendered only when filled
href null Stretched link over the item
navigate false wire:navigate on that link
action null Alpine expression; ignored with href
<x-wirelist.empty> The slot is the message

Slots: the list takes footer; the item takes leading, badges, aside, actions, header, title and the default slot. Other attributes merge onto the outer element.

Token Default
--wtl-item-padding 14px 20px
--wtl-item-gap 8px
--wtl-subtitle-font-size 0.875rem
--wtl-grid-min 16rem
--wtl-grid-gap 16px
--wtl-cover-ratio 16 / 10

To change an item's markup, copy it to resources/views/components/wirelist/item.blade.php in your app, which takes precedence over the package's; it then stops receiving updates.

built and maintained by Edu Lazaro · MIT license