Skip to main content

Data display

Table code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/table.blade.php

@props([
    'striped' => false,
    'stackable' => false,
    'flush' => false,
    'sticky' => false,
    'maxHeight' => '24rem',
])

@php
    /*
    | Responsive table shell. Compose with x-ui.table.head, x-ui.table.row,
    | x-ui.table.heading and x-ui.table.cell.
    |
    | Set `flush` to drop the outer border/rounding when embedding inside a card.
    | Set `sticky` to pin the header while the body scrolls (bounded by
    | `maxHeight`). Pass `resizable` to individual headings for drag-to-resize
    | columns, and `expandable` to rows for a toggleable detail row.
    |
    | Usage:
    |   <x-ui.table>
    |       <x-slot:head>
    |           <x-ui.table.heading>Name</x-ui.table.heading>
    |       </x-slot:head>
    |       <x-ui.table.row><x-ui.table.cell>Ada</x-ui.table.cell></x-ui.table.row>
    |   </x-ui.table>
    |
    |   <x-ui.table sticky max-height="20rem">...</x-ui.table>
    |
    |   <x-ui.card padding="none">
    |       <x-ui.card.header title="Team" />
    |       <x-ui.table flush>...</x-ui.table>
    |   </x-ui.card>
    */
    $wrapperClass = ($sticky ? 'overflow-auto' : 'overflow-x-auto').($stackable ? ' max-md:overflow-visible' : '');

    if (! $flush) {
        $wrapperClass .= ' rounded-2xl border border-gray-200 dark:border-gray-800';
    }

    $stickyClass = $sticky
        ? ' [&_thead_th]:sticky [&_thead_th]:top-0 [&_thead_th]:z-10 [&_thead_th]:bg-gray-50 dark:[&_thead_th]:bg-gray-900 [&_thead]:shadow-[0_1px_0_0_var(--color-gray-200)] dark:[&_thead]:shadow-[0_1px_0_0_var(--color-gray-800)]'
        : '';
@endphp

<div {{ $attributes->merge(['class' => $wrapperClass]) }} @if ($sticky) style="max-height: {{ $maxHeight }}" @endif>
    <table
        @if ($stackable) data-stackable @endif
        class="min-w-full divide-y divide-gray-200 dark:divide-gray-800 {{ $stackable ? 'max-md:block' : '' }} {{ $striped ? '[&_tbody_tr:nth-child(odd)]:bg-gray-50/60 dark:[&_tbody_tr:nth-child(odd)]:bg-gray-800/30' : '' }}{{ $stickyClass }}"
    >
        @isset($head)
            <thead class="bg-gray-50 dark:bg-gray-900/60 {{ $stackable ? 'hidden md:table-header-group' : '' }}">
                <tr>{{ $head }}</tr>
            </thead>
        @endisset

        <tbody class="divide-y divide-gray-200 dark:divide-gray-800 bg-white dark:bg-gray-900 {{ $stackable ? 'max-md:block max-md:divide-y-0' : '' }}">
            {{ $slot }}
        </tbody>
    </table>
</div>

resources/views/components/ui/table/cell.blade.php

@props([
    'align' => 'left',
    'muted' => false,
    'label' => null,
    'summary' => false,
    'stackable' => false,
])

@php
    /* Table body cell. align: left, center, right */
    $alignClass = ['left' => 'text-left', 'center' => 'text-center', 'right' => 'text-right'][$align] ?? 'text-left';
    $color = $muted ? 'text-gray-500 dark:text-gray-400' : 'text-gray-700 dark:text-gray-200';
    $justify = ['left' => 'justify-start', 'center' => 'justify-center', 'right' => 'justify-end'][$align] ?? 'justify-start';
@endphp

@if ($stackable && $summary)
    <td
        x-on:click="if (window.matchMedia('(max-width: 767px)').matches) expanded = ! expanded"
        x-bind:aria-expanded="expanded"
        {{ $attributes->merge(['class' => "block w-full cursor-pointer px-5 py-4 text-sm md:cursor-default md:table-cell md:px-6 md:whitespace-nowrap {$alignClass} {$color}"]) }}
    >
        <div class="flex items-center justify-between gap-3 md:block">
            <div class="min-w-0 flex-1">
                {{ $slot }}
            </div>
            <x-ui.icon
                name="caret-down"
                weight="bold"
                size="sm"
                class="shrink-0 text-gray-400 transition-transform duration-200 md:hidden"
                x-bind:class="expanded && 'rotate-180'"
            />
        </div>
    </td>
@elseif ($stackable)
    <td
        x-on:click.stop
        {{ $attributes->merge(['class' => 'max-md:hidden max-md:group-data-[expanded=true]:block text-sm md:table-cell md:whitespace-nowrap md:px-6 md:py-4']) }}
    >
        <div class="flex items-center justify-between gap-4 border-t border-gray-100 px-5 py-3 md:contents md:border-0 md:px-0 md:py-0 dark:border-gray-800">
            @if ($label)
                <span class="shrink-0 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 md:hidden">
                    {{ $label }}
                </span>
            @endif
            <div class="flex min-w-0 flex-1 items-center gap-2 {{ $justify }} md:block {{ $alignClass }} {{ $color }}">
                {{ $slot }}
            </div>
        </div>
    </td>
@else
    <td {{ $attributes->merge(['class' => "px-6 py-4 text-sm whitespace-nowrap {$alignClass} {$color}"]) }}>
        {{ $slot }}
    </td>
@endif

resources/views/components/ui/table/heading.blade.php

@props([
    'align' => 'left',
    'sortable' => false,
    'sort' => null,
    'href' => null,
    'resizable' => false,
])

@php
    /*
    | Table column header cell.
    |
    | align:     left, center, right
    | sortable:  render a sort affordance with an indicator
    | sort:      current direction for this column — 'asc', 'desc' or null
    | href:      link to toggle sorting (falls back to a plain button)
    | resizable: add a drag handle on the trailing edge to resize the column
    |
    | Usage:
    |   <x-ui.table.heading>Name</x-ui.table.heading>
    |   <x-ui.table.heading sortable sort="asc" :href="route('admin.users.index', ['sort' => 'name'])">Name</x-ui.table.heading>
    |   <x-ui.table.heading resizable>Name</x-ui.table.heading>
    */
    $alignClass = ['left' => 'text-left', 'center' => 'text-center', 'right' => 'text-right'][$align] ?? 'text-left';

    $sortIcon = match ($sort) {
        'asc' => 'caret-up',
        'desc' => 'caret-down',
        default => 'arrows-down-up',
    };

    $justify = ['left' => 'justify-start', 'center' => 'justify-center', 'right' => 'justify-end'][$align] ?? 'justify-start';
@endphp

<th
    scope="col"
    @if ($resizable)
        x-data="{
            begin(event) {
                const th = this.$el;
                const startX = event.clientX;
                const startWidth = th.offsetWidth;
                const move = (e) => {
                    th.style.width = Math.max(64, startWidth + e.clientX - startX) + 'px';
                };
                const up = () => {
                    window.removeEventListener('mousemove', move);
                    window.removeEventListener('mouseup', up);
                };
                window.addEventListener('mousemove', move);
                window.addEventListener('mouseup', up);
            },
        }"
    @endif
    {{ $attributes->merge(['class' => "px-6 py-3.5 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 {$alignClass}".($resizable ? ' relative' : '')]) }}
>
    @if ($sortable)
        <{{ $href ? 'a' : 'button' }}
            @if ($href) href="{{ $href }}" @else type="button" @endif
            class="group inline-flex items-center gap-1.5 {{ $justify }} font-semibold uppercase tracking-wider transition hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer"
        >
            {{ $slot }}
            <x-ui.icon
                :name="$sortIcon"
                weight="bold"
                size="xs"
                class="{{ $sort ? 'text-brand-500' : 'text-gray-300 dark:text-gray-600 group-hover:text-gray-400' }}"
            />
        </{{ $href ? 'a' : 'button' }}>
    @else
        {{ $slot }}
    @endif

    @if ($resizable)
        <span
            @mousedown.prevent="begin($event)"
            class="absolute inset-y-0 -end-1 z-10 w-2 cursor-col-resize select-none rounded transition-colors hover:bg-brand-400/50"
            aria-hidden="true"
        ></span>
    @endif
</th>

resources/views/components/ui/table/row.blade.php

@props([
    'hover' => true,
    'stackable' => false,
    'expandable' => false,
    'colspan' => 100,
])

@php
    /*
    | Table body row. Set `expandable` and provide an `expansion` slot to reveal
    | a full-width detail row on click; a caret cell is appended to the main row
    | automatically.
    |
    | Usage:
    |   <x-ui.table.row expandable>
    |       <x-ui.table.cell>Ada</x-ui.table.cell>
    |       <x-slot:expansion>Extra detail about Ada.</x-slot:expansion>
    |   </x-ui.table.row>
    */
    $hoverClass = $hover ? 'md:hover:bg-gray-50 md:dark:hover:bg-gray-800/40' : '';
@endphp

@if ($expandable)
    @php
        $rowKey = 'expand-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
        $detailId = $rowKey.'-detail';
    @endphp
    <tr
        x-data="{
            expanded: false,
            toggle() {
                this.expanded = ! this.expanded;
                this.$dispatch('{{ $rowKey }}', { expanded: this.expanded });
            },
        }"
        x-on:click="toggle()"
        {{ $attributes->merge(['class' => 'cursor-pointer transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/40']) }}
    >
        {{ $slot }}
        <td class="w-10 pe-4 text-end">
            <button
                type="button"
                aria-controls="{{ $detailId }}"
                aria-expanded="false"
                x-bind:aria-expanded="expanded.toString()"
                class="inline-flex cursor-pointer items-center justify-center rounded p-1 text-gray-400 transition-colors hover:text-gray-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 dark:hover:text-gray-200"
            >
                <span class="sr-only">Toggle row details</span>
                <x-ui.icon
                    name="caret-down"
                    weight="bold"
                    size="sm"
                    class="inline-block transition-transform duration-200"
                    x-bind:class="expanded && 'rotate-180'"
                />
            </button>
        </td>
    </tr>
    <tr
        id="{{ $detailId }}"
        x-data="{ show: false }"
        x-on:{{ $rowKey }}.window="show = $event.detail.expanded"
        x-show="show"
        x-cloak
        class="bg-gray-50/70 dark:bg-gray-800/30"
    >
        <td colspan="{{ (int) $colspan }}" class="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
            {{ $expansion ?? '' }}
        </td>
    </tr>
@else
    <tr
        @if ($stackable)
            x-data="{ expanded: false }"
            x-bind:data-expanded="expanded ? 'true' : 'false'"
        @endif
        {{ $attributes->merge([
            'class' => $stackable
                ? "group block border-b border-gray-200 transition-colors md:table-row md:border-0 dark:border-gray-800 {$hoverClass}"
                : ($hover ? 'transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/40' : ''),
        ]) }}
    >
        {{ $slot }}
    </tr>
@endif