Loading States

Gale provides two directives for handling loading states: x-loading for declarative show/hide behavior, and x-indicator for programmatic state management. Both automatically track requests within their scope.

The x-loading Directive

The x-loading directive shows elements during loading and hides them when idle. It listens for gale:started and gale:finished events automatically.

<div x-data="{ query: '' }">
    <input x-model="query" placeholder="Search...">
    <button @click="$get('/search')">Search</button>

    <!-- Shows during loading, hidden when idle -->
    <span x-loading>Searching...</span>
</div>

Inverse Loading (.remove)

Use the .remove modifier to hide content during loading and show it when idle. This is useful for swapping button text or hiding content.

<button @click="$postx('/save')">
    <!-- Normal text, hidden during loading -->
    <span x-loading.remove>Save Changes</span>

    <!-- Loading text, shown during loading -->
    <span x-loading>Saving...</span>
</button>

Adding Classes (.class)

The .class modifier adds CSS classes during loading and removes them when idle. This is perfect for visual feedback like reducing opacity or changing cursor.

<!-- Add opacity and cursor classes during loading -->
<button
    @click="$postx('/submit')"
    x-loading.class="opacity-50 cursor-wait"
    class="bg-blue-500 text-white px-4 py-2 rounded">
    Submit
</button>

<!-- Add loading state to entire form -->
<form x-loading.class="pointer-events-none opacity-60">
    <!-- Form fields -->
</form>

Setting Attributes (.attr)

The .attr modifier sets an attribute during loading and removes it when idle. Commonly used to disable buttons and prevent double submissions.

<!-- Disable button during loading -->
<button @click="$postx('/save')" x-loading.attr="disabled">
    Save
</button>

<!-- Set aria-busy for accessibility -->
<div x-loading.attr="aria-busy">
    <!-- Content -->
</div>

<!-- Make form readonly during loading -->
<input x-loading.attr="readonly" type="text">

Delay Loading State (.delay)

The .delay modifier prevents loading indicators from flashing on fast requests. The loading state only shows if the request takes longer than the specified delay.

<!-- Only show if request takes more than 150ms -->
<span x-loading.delay.150ms>Loading...</span>

<!-- 500ms delay for slower operations -->
<div x-loading.delay.500ms class="spinner">
    <!-- Spinner animation -->
</div>

<!-- Combine with other modifiers -->
<button x-loading.delay.200ms.class="opacity-50">
    Submit
</button>

The x-indicator Directive

The x-indicator directive creates a reactive state variable that tracks loading status. This gives you full programmatic control over loading behavior.

<div
    x-data="{ loading: false, results: [] }"
    x-indicator="loading">

    <button @click="$get('/search')" :disabled="loading">
        Search
    </button>

    <!-- Use loading state in templates -->
    <span x-show="loading">Searching...</span>

    <!-- Use in conditional classes -->
    <div :class="loading && 'animate-pulse'">
        <!-- Content -->
    </div>
</div>

Default State Name

When no expression is provided, x-indicator defaults to using loading as the state variable name.

<!-- These are equivalent -->
<div x-data="{ loading: false }" x-indicator>
    <span x-show="loading">Loading...</span>
</div>

<div x-data="{ loading: false }" x-indicator="loading">
    <span x-show="loading">Loading...</span>
</div>

Custom State Names

Use custom state names to track different operations independently:

<div x-data="{ saving: false, deleting: false }">

    <!-- Track save operation -->
    <div x-indicator="saving">
        <button @click="$postx('/save')" :disabled="saving">
            <span x-text="saving ? 'Saving...' : 'Save'"></span>
        </button>
    </div>

    <!-- Track delete operation separately -->
    <div x-indicator="deleting">
        <button @click="$deletex('/item')" :disabled="deleting">
            <span x-text="deleting ? 'Deleting...' : 'Delete'"></span>
        </button>
    </div>

    <!-- Disable entire form during any operation -->
    <fieldset :disabled="saving || deleting">
        <!-- Form fields -->
    </fieldset>
</div>

Loading Scope

Both x-loading and x-indicator track requests from the element and any of its descendants. They use the Alpine x-data scope to determine which requests are relevant.

<div x-data="{ items: [] }">
    <!-- This loading indicator responds to any request inside this div -->
    <span x-loading>Loading...</span>

    <button @click="$get('/items')">Load Items</button>
    <button @click="$postx('/refresh')">Refresh</button>
</div>

<!-- Separate scope - its loading state is independent -->
<div x-data="{ user: null }">
    <span x-loading>Loading user...</span>
    <button @click="$get('/user')">Load User</button>
</div>

Multiple Concurrent Requests

Both directives correctly handle multiple concurrent requests. The loading state stays active until all requests complete.

<div x-data="{ users: [], posts: [], comments: [] }">
    <!-- Shows while ANY request is in progress -->
    <div x-loading class="text-center py-4">
        <div class="spinner"></div>
        Loading data...
    </div>

    <!-- Load multiple resources at once -->
    <button @click="
        $get('/users');
        $get('/posts');
        $get('/comments');
    ">
        Load All Data
    </button>
</div>

x-loading vs x-indicator

Feature x-loading x-indicator
Approach Declarative Programmatic
State variable Not needed Required in x-data
Show/hide Automatic Manual (x-show)
Class toggle .class modifier :class binding
Attribute toggle .attr modifier :disabled binding
Delay support .delay modifier Not built-in
Use in JS Not possible Full access
Best for Simple UI feedback Complex logic

Combining Both Directives

You can use both directives together for maximum flexibility:

<div
    x-data="{ loading: false, data: null }"
    x-indicator="loading">

    <!-- Use x-indicator for logic -->
    <button
        @click="$get('/data')"
        :disabled="loading"
        :class="loading && 'cursor-wait'">
        <!-- Use x-loading for text swapping -->
        <span x-loading.remove>Fetch Data</span>
        <span x-loading>Loading...</span>
    </button>

    <!-- Use loading state for conditional rendering -->
    <template x-if="!loading && data">
        <div x-text="data.message"></div>
    </template>
</div>

Complete Example

<div
    x-data="{
        query: '',
        results: [],
        loading: false
    }"
    x-indicator="loading"
    class="max-w-lg mx-auto">

    <!-- Search form -->
    <div class="flex gap-2">
        <input
            x-model="query"
            x-loading.attr="readonly"
            type="text"
            placeholder="Search..."
            class="flex-1 border rounded px-3 py-2">

        <button
            @click="$get('/search')"
            :disabled="loading || !query"
            x-loading.class="opacity-50 cursor-wait"
            class="bg-blue-500 text-white px-4 py-2 rounded">
            <span x-loading.remove>Search</span>
            <span x-loading>Searching...</span>
        </button>
    </div>

    <!-- Loading indicator with delay to prevent flicker -->
    <div x-loading.delay.200ms class="mt-4 text-center">
        <div class="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto"></div>
        <p class="mt-2 text-gray-600">Searching for "<span x-text="query"></span>"...</p>
    </div>

    <!-- Results -->
    <ul x-loading.remove class="mt-4 space-y-2">
        <template x-for="result in results" :key="result.id">
            <li class="p-3 bg-gray-50 rounded" x-text="result.title"></li>
        </template>
    </ul>

    <!-- Empty state -->
    <p
        x-show="!loading && query && results.length === 0"
        class="mt-4 text-gray-500 text-center">
        No results found
    </p>
</div>

On this page