Documentation menu
HomeDocsCustomizationLoader screen

Loader

The Martis Loader is a built-in loading indicator that appears automatically during data fetches, page transitions, and async operations. It is fully configurable and replaceable without forking the package.

Where It Appears

ContextTriggerDefault Size
Resource Index (initial load)Schema + records fetchlg
Resource Index (refetch overlay)Search, sort, paginationsm (overlay)
Resource Index / Lens (failed fetch)Records request failed (5xx, network)Inline QueryErrorState + Retry replaces the table body; overlay covers it during the retry (v1.32.4)
Resource DetailSchema + record fetchlg
Resource LensSchema + lens querymd
ProfileSession checkmd
Action DrawerAction schema fetchmd
Tool Page/api/tools/{uriKey} fetchmd
Cache AdminCache stats endpointmd

size defaults to md when the call site does not pass it. The lg and sm rows above are the explicit overrides used by the resource pages.

Configuration

Publish the Martis config file if you haven't already:

php artisan martis:vendor-publish --config

Then edit config/martis.php and add or modify the loader section:

'loader' => [
// Custom loading text. null = use the locale default ("Loading...")
'message' => null,
 
// Phosphor icon name to replace the spinner (e.g. 'spinner-gap', 'circle-notch')
// When set, the named icon spins instead of the default SpinnerGap.
'icon' => null,
 
// URL to a logo/image shown instead of the spinner, scaled to fit
// its square. Takes precedence over 'icon'.
'logo' => env('MARTIS_LOADER_LOGO'),
 
// Dark theme variant of 'logo' (v2.6.0). If only one of the pair is
// set, it serves both themes.
'logoDark' => env('MARTIS_LOADER_LOGO_DARK'),
 
// CSS color for the spinner. Default: var(--martis-accent) — your theme's accent color.
'spinnerColor' => null,
 
// Overlay background opacity (0.0–1.0). Default: 0.6.
'overlayOpacity' => null,
 
// CSS color for overlay background. Default: var(--martis-bg).
'overlayColor' => null,
 
// Set to true to disable all loaders globally.
'disabled' => false,
 
// Granular opt-out per context.
'disableOn' => [
'table' => false, // disable overlay on index table refetch
'search' => false, // disable loader during search
'detail' => false, // disable loader on resource detail pages
'components' => false, // disable in other component contexts
// (Profile, ActionDrawer, ToolPage, CacheAdmin, ResourceLens)
],
],

All options are optional. Omitted keys use the built-in defaults.

Configuration Examples

Custom message and spinner color

'loader' => [
'message' => 'Please wait...',
'spinnerColor' => '#6366f1',
],

Use your brand logo instead of a spinner

'loader' => [
'logo' => '/images/logo.svg',
'message' => null, // hide message when using logo
],

Or from .env, with a dark theme variant (v2.6.0), so a mark drawn for light backgrounds does not show on the dark overlay:

MARTIS_LOADER_LOGO=/brand/mark-light.svg
MARTIS_LOADER_LOGO_DARK=/brand/mark-dark.svg

Both images render and CSS shows the one of the active theme, like the brand logo. A config published before v2.6.0 has 'logo' => null and no logoDark key, so it ignores these variables until you copy the two lines into its loader block.

Use a different Phosphor icon

'loader' => [
'icon' => 'circle-notch',
'message' => 'Fetching...',
],

Semi-transparent overlay on table refetch

'loader' => [
'overlayOpacity' => 0.4,
'overlayColor' => '#0f172a',
],

Disable all loaders

'loader' => [
'disabled' => true,
],

Disable only the table refetch overlay

'loader' => [
'disableOn' => [
'table' => true,
],
],

Customization via Component Registry

For full control, replace MartisLoader with your own component. Register the override from your consumer extension bundle (resources/js/martis-extensions/) (typically resources/js/martis-extensions/index.ts):

import { componentRegistry } from '@martis/runtime'
import { MyLoader } from './MyLoader'
 
// Replace the global loader with your custom component. Every Martis
// page, and every `MartisLoader` an extension renders from
// `@martis/runtime`, routes through this override.
componentRegistry.register('loader', MyLoader)

The override is read on every render, so registering after the SPA has mounted (HMR / lazy-loaded boot files) takes effect immediately on the next render — no reload required.

Your custom loader receives the same props as MartisLoader:

interface MartisLoaderProps {
/** Whether the loader is visible */
loading?: boolean
/** Override config message */
message?: string | null
/** Overlay mode: covers parent container with semi-transparent overlay */
overlay?: boolean
/** Disable this specific loader (per-context opt-out) */
disabled?: boolean
/** Size variant */
size?: 'sm' | 'md' | 'lg'
/** Children to render under the loader overlay (overlay mode only) */
children?: React.ReactNode
/**
* Context identifier used to check `loader.disableOn` config.
* Supported: "table" | "search" | "detail" | "components".
* Pages pass this so a single config flag can opt one surface out
* without having to thread `disabled={...}` everywhere.
*/
context?: 'table' | 'search' | 'detail' | 'components'
/**
* Per-instance config override. Merged on top of the global
* `config.loader` for this one render. The wrapper passes the
* active resource's `Resource::loaderConfig()` here automatically;
* call sites can also pass an inline override to win over both
* global config and the resource override.
*/
configOverride?: Partial<MartisLoaderConfig>
}

Size tokens

SizeSpinnerText
sm16pxtext-xs (12px)
md (default)24pxtext-sm (14px)
lg32pxtext-base (16px)

Custom Loader Example

// resources/js/martis-extensions/MyLoader.tsx
import type { MartisLoaderProps } from '@martis/runtime'
 
export function MyLoader({ loading, size, overlay, children }: MartisLoaderProps) {
if (!loading) return overlay ? <>{children}</> : null
 
const spinner = (
<div className="flex items-center gap-2 text-indigo-500">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="text-sm text-muted-foreground">Loading...</span>
</div>
)
 
if (overlay) {
return (
<div className="relative">
{children}
<div className="absolute inset-0 flex items-center justify-center bg-background/60 rounded-lg z-10">
{spinner}
</div>
</div>
)
}
 
return spinner
}

Per-resource override — Resource::loaderConfig()

A Resource can ship its own loader override that wins over the global config while a user is inside that resource's pages:

namespace App\Martis\Resources;
 
use Martis\Resource;
 
class AuditLog extends Resource
{
public static function loaderConfig(): array
{
return [
'message' => 'Calibrating audit log…',
'spinnerColor' => '#FF6347',
'disableOn' => [
'detail' => true, // skip the detail-page loader
],
];
}
 
// …model(), fields(), etc.
}

Recognised keys mirror config/martis.php loader: message, icon, logo, spinnerColor, overlayOpacity, overlayColor, disabled, disableOn (table | search | detail | components).

The override is merged shallow-on-top of config('martis.loader'). disableOn is shallow-merged separately so opting one context in/out doesn't clobber the others. The override is active only while a ResourceIndex or ResourceDetail page for that resource is mounted; navigating away restores the global config.

Returns [] by default. Defining the method is opt-in.

Bridge — config → frontend

The PHP-side loader config reaches the SPA via window.MartisConfig.loader, set by the package blade template (resources/views/app.blade.php, in the inline window.MartisConfig script):

loader: {!! json_encode(config('martis.loader', ['disabled' => false])) !!},

The TypeScript shape is MartisLoaderConfig, a type @martis/runtime exports (v1.38.0+). If you need to gate the loader from the same closure that already drives a feature flag (e.g. dim the spinner during a maintenance window), point MARTIS_LOADER_DISABLED at an env value the closure reads — the env wrapper avoids editing the published config file.

Publishing via martis:vendor-publish

To customize the configuration:

# Publish only the config file
php artisan martis:vendor-publish --config
 
# Force overwrite if already published
php artisan martis:vendor-publish --config --force

After publishing, edit config/martis.php in your application root. The config is automatically passed to the frontend via window.MartisConfig.loader.

To customize translations (loading messages):

# Publish language files
php artisan martis:vendor-publish --lang

Then edit lang/vendor/martis/{locale}/messages.php and change the loading key:

// lang/vendor/martis/en/messages.php
return [
'loading' => 'Fetching data...',
// ...
];

Testing the Loader

Trigger the initial page loader

Navigate to any resource index or detail page. The loader appears immediately while data is being fetched and disappears once the response arrives.

http://your-app.test/martis/resources/posts

Trigger the refetch overlay

On a resource index page, type in the search bar. After 300ms debounce, a small overlay spinner appears over the table while results load. If the backend is fast, add artificial latency with sleep(1) in your Resource's indexQuery() method.

Trigger the overlay with artificial latency

In your Resource class (for testing only):

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
 
public static function indexQuery(Request $request, Builder $query): Builder
{
sleep(2); // simulate slow response — remove after testing
return parent::indexQuery($request, $query);
}

Verify each configuration option

OptionHow to test
messageSet a custom string; check text next to spinner
iconSet a valid Phosphor icon name; spinner should change
logoSet a URL to an image; image should appear instead of spinner
spinnerColorSet a CSS hex color; spinner color should change
overlayOpacitySet to 0.2 vs 0.9; overlay darkness should differ
overlayColorSet to a bright color; overlay background color should change
disabledSet true; no loader should appear anywhere
disableOn.tableSet true; search/sort refetch should show no overlay

Verify component override

After registering a custom component via componentRegistry.register('loader', MyLoader), reload any resource page and confirm your component renders instead of the default spinner.