Skip to main content

Data display

Loading State code

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

resources/views/components/ui/loading-state.blade.php

@props([
    'label' => 'Working',
    'showElapsed' => true,
    'elapsed' => 0,
    'shape' => 'square',
])

@php
    /*
    | Compact loading state with a pixel-grid indicator, shimmering label and
    | optional elapsed timer. Use the spinner for icon-sized loading feedback.
    |
    | Usage:
    |   <x-ui.loading-state label="Analysing data" />
    |   <x-ui.loading-state label="Connecting" shape="circle" :show-elapsed="false" />
    |   <x-ui.loading-state label="Resuming" :elapsed="12.4" />
    */
    $pixelDelays = [90, 180, 270, 0, 90, 180, 90, 180, 270];
    $resolvedShape = in_array($shape, ['square', 'dots', 'circle', 'diamond', 'ring'], true) ? $shape : 'square';
    $circlePositions = [
        '-top-px left-1/2 -translate-x-1/2',
        'right-0 top-0.5',
        '-right-px top-1/2 -translate-y-1/2',
        'right-0 bottom-0.5',
        '-bottom-px left-1/2 -translate-x-1/2',
        'bottom-0.5 left-0',
        '-left-px top-1/2 -translate-y-1/2',
        'left-0 top-0.5',
    ];
    $diamondPositions = [2, 4, 5, 6, 8];
@endphp

<span
    x-data="{
        elapsed: Number(@js($elapsed)),
        startedAt: performance.now() - (Number(@js($elapsed)) * 1000),
        timer: null,
        init() {
            if (! @js((bool) $showElapsed)) {
                return;
            }

            this.timer = setInterval(() => {
                this.elapsed = (performance.now() - this.startedAt) / 1000;
            }, 100);
        },
        destroy() {
            clearInterval(this.timer);
        },
    }"
    role="status"
    aria-live="polite"
    aria-label="{{ $label }}"
    {{ $attributes->merge(['class' => 'inline-flex w-fit items-center gap-2.5 text-brand-600 dark:text-brand-400']) }}
>
    <span aria-hidden="true" data-loading-shape="{{ $resolvedShape }}" class="inline-grid size-[18px] shrink-0 place-items-center">
        @if ($resolvedShape === 'dots')
            <span class="flex items-center gap-1">
                @foreach ([0, 120, 240] as $delay)
                    <span
                        class="loading-state-pixel size-1 rounded-full bg-current motion-reduce:animate-none"
                        style="--loading-state-delay: {{ $delay }}ms"
                    ></span>
                @endforeach
            </span>
        @elseif ($resolvedShape === 'circle')
            <span class="relative block size-[18px]">
                @foreach ($circlePositions as $index => $position)
                    <span
                        class="loading-state-pixel absolute size-[3px] rounded-full bg-current {{ $position }} motion-reduce:animate-none"
                        style="--loading-state-delay: {{ $index * 75 }}ms"
                    ></span>
                @endforeach
            </span>
        @elseif ($resolvedShape === 'diamond')
            <span class="grid grid-cols-3 gap-[2px]">
                @foreach (range(1, 9) as $position)
                    @if (in_array($position, $diamondPositions, true))
                        <span
                            class="loading-state-pixel size-[4px] rotate-45 rounded-[0.5px] bg-current motion-reduce:animate-none"
                            style="--loading-state-delay: {{ $position * 55 }}ms"
                        ></span>
                    @else
                        <span class="size-[4px]"></span>
                    @endif
                @endforeach
            </span>
        @elseif ($resolvedShape === 'ring')
            <span class="relative block size-[18px] animate-spin rounded-full border border-current/25 motion-reduce:animate-none">
                <span class="absolute -top-0.5 left-1/2 size-1 -translate-x-1/2 rounded-full bg-current"></span>
                <span class="absolute -bottom-px left-1/2 size-0.5 -translate-x-1/2 rounded-full bg-current opacity-40"></span>
            </span>
        @else
            <span class="grid grid-cols-3 gap-[1.5px]">
                @foreach ($pixelDelays as $delay)
                    <span
                        class="loading-state-pixel size-1 rounded-[1px] bg-current motion-reduce:animate-none"
                        style="--loading-state-delay: {{ $delay }}ms"
                    ></span>
                @endforeach
            </span>
        @endif
    </span>

    <span class="loading-state-label bg-[length:200%_100%] bg-clip-text text-[13px] font-medium text-transparent motion-reduce:animate-none">
        {{ $label }}
    </span>

    @if ($showElapsed)
        <span
            aria-hidden="true"
            class="font-mono text-xs tabular-nums text-brand-700/70 dark:text-brand-300/70"
            x-text="`${elapsed.toFixed(1)}s`"
        >{{ number_format((float) $elapsed, 1) }}s</span>
    @endif
</span>