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
| Context | Trigger | Default Size |
|---|---|---|
| Resource Index (initial load) | Schema + records fetch | lg |
| Resource Index (refetch overlay) | Search, sort, pagination | sm (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 Detail | Schema + record fetch | lg |
| Resource Lens | Schema + lens query | md |
| Profile | Session check | md |
| Action Drawer | Action schema fetch | md |
| Tool Page | /api/tools/{uriKey} fetch | md |
| Cache Admin | Cache stats endpoint | md |
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 --configThen 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.svgMARTIS_LOADER_LOGO_DARK=/brand/mark-dark.svgBoth 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
| Size | Spinner | Text |
|---|---|---|
sm | 16px | text-xs (12px) |
md (default) | 24px | text-sm (14px) |
lg | 32px | text-base (16px) |
Custom Loader Example
// resources/js/martis-extensions/MyLoader.tsximport 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 filephp artisan martis:vendor-publish --config # Force overwrite if already publishedphp artisan martis:vendor-publish --config --forceAfter 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 filesphp artisan martis:vendor-publish --langThen edit lang/vendor/martis/{locale}/messages.php and change the loading key:
// lang/vendor/martis/en/messages.phpreturn [ '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/postsTrigger 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
| Option | How to test |
|---|---|
message | Set a custom string; check text next to spinner |
icon | Set a valid Phosphor icon name; spinner should change |
logo | Set a URL to an image; image should appear instead of spinner |
spinnerColor | Set a CSS hex color; spinner color should change |
overlayOpacity | Set to 0.2 vs 0.9; overlay darkness should differ |
overlayColor | Set to a bright color; overlay background color should change |
disabled | Set true; no loader should appear anywhere |
disableOn.table | Set 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.