Skip to content
Brok UI

Loading…

No results

Loaders

Open source Core

A loading-ui.com-inspired family of animated loaders — spinners, dots, orbital, bars, square grids, shapes, text and a few playful ones — all token-themed and reduced-motion safe.

Version
v1.2.1
Stability
stable
License
MIT
Related
Spinner
Dots Loader
Orbit Loader
Bars Loader
Grid Loader
Shape Loader
Text Loader
Terminal Loader
Scan Loader
10 of 10

Eyes Loader — A wandering-eyes loader — two eyes whose pupils look left, right, up and down in sync, with an occasional blink; the pupils follow the pointer.

Preview

Loading sm
Loading md
Loading lg
previews.components.eyes-loader.default.blade.php Blade
<div class="flex flex-wrap items-end gap-8">
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="sm" /><span class="text-xs text-muted-foreground">sm</span></div>
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="md" /><span class="text-xs text-muted-foreground">md</span></div>
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="lg" /><span class="text-xs text-muted-foreground">lg</span></div>
</div>

Installation

terminal
php artisan ui:add eyes-loader

Note

This component ships an Alpine behavior module at resources/js/ui/eyes-loader.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './eyes-loader.js';

Registry contract

php artisan ui:add eyes-loader writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/eyes-loader.blade.php
  • js resources/js/ui/eyes-loader.js
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

eyes-loader.md
# Brok UI: Eyes Loader (`eyes-loader`)

A wandering-eyes loader — two eyes whose pupils look left, right, up and down in sync, with an occasional blink; the pupils follow the pointer.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add eyes-loader
```

## Usage

```blade
<div class="flex flex-wrap items-end gap-8">
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="sm" /><span class="text-xs text-muted-foreground">sm</span></div>
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="md" /><span class="text-xs text-muted-foreground">md</span></div>
    <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="lg" /><span class="text-xs text-muted-foreground">lg</span></div>
</div>
```

## Props

- `label` (string, default `Loading`) — Accessible status label announced to screen readers.
- `size` (md|sm|lg, default `md`) — Eye and pupil size: sm, md, or lg.
- `variant` (string, default `default`) — Reserved for future looks; any value currently renders the same default look.
- `follow` (bool, default `true`) — Pupils track the pointer when true; false keeps the CSS wandering motion only.

## Use when

- Use while content or system work is in progress so users understand the interface is responding.
- Adding a playful, attention-catching loading indicator to an empty or waiting state, such as a chatbot or search widget.
- Wanting the pupils to track the pointer for a lightweight, characterful touch.

## Avoid when

- Do not use indefinite loading indicators when you can show structure, progress, or a next step instead.
- Needing a neutral, brand-agnostic loading indicator; use spinner or dots-loader instead.
- Building a dense, professional dashboard; the wandering eyes read as informal.

## Anti-patterns

- Indefinite loading with no progress or recovery

## Rules

- Use the `<brok:eyes-loader>` tag (or `<x-ui.eyes-loader>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/eyes-loader
- Registry JSON (files, props, contract): https://brokui.dev/r/open/eyes-loader.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
label string Loading Accessible status label announced to screen readers.
size md | sm | lg md Eye and pupil size: sm, md, or lg.
variant string default Reserved for future looks; any value currently renders the same default look.
follow bool true Pupils track the pointer when true; false keeps the CSS wandering motion only.

Slots

  • default — Unused; the component has no visible default slot content.

Data slots

Stable hooks for CSS overrides and browser tests.

eyes-loader

Behavior

  • Pupils look left, right, up, and down on a loop, with an occasional blink, driven by CSS keyframes.
  • When follow is true, eyes-loader.js tracks the pointer and sets --eye-dx/--eye-dy custom properties so the pupils track it instead of wandering.
  • Only the default variant currently ships; any other value falls back to it.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized, reducedMotion, interactive.

Guidance

Loading feedback

Show that content or system work is in progress.

Use when

  • Use while content or system work is in progress so users understand the interface is responding.
  • Adding a playful, attention-catching loading indicator to an empty or waiting state, such as a chatbot or search widget.
  • Wanting the pupils to track the pointer for a lightweight, characterful touch.

Avoid when

  • Do not use indefinite loading indicators when you can show structure, progress, or a next step instead.
  • Needing a neutral, brand-agnostic loading indicator; use spinner or dots-loader instead.
  • Building a dense, professional dashboard; the wandering eyes read as informal.

Use instead

  • Skeleton for known layout
  • Determinate progress for measurable work

Anti-patterns

  • Indefinite loading with no progress or recovery
Anatomy
root
Theming hooks
eyes-loader

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
none
  • The root carries role="status" and an aria-label built from the label prop so screen readers announce the loading state once.
  • Eyes and pupils are decorative and hidden from assistive tech with aria-hidden.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="eyes-loader-{{ $record->id }}">
    <div class="flex flex-wrap items-end gap-8">
        <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="sm" /><span class="text-xs text-muted-foreground">sm</span></div>
        <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="md" /><span class="text-xs text-muted-foreground">md</span></div>
        <div class="flex flex-col items-center gap-3"><x-ui.eyes-loader size="lg" /><span class="text-xs text-muted-foreground">lg</span></div>
    </div>
</div>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/eyes-loader.blade.php Blade
@props([
    'label' => 'Loading',
    'size' => 'md',
    'variant' => 'default',
    // The pupils follow the pointer (fine pointers, motion allowed); false
    // keeps the CSS wandering only.
    'follow' => true,
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    // Wandering-eyes loader, faithful to loading-ui.com. Two side-by-side eyes
    // (bg-card surface, border-border) each hold a foreground pupil that looks
    // around — left, right, up, down — in sync, with an occasional blink. A
    // single look ships (variant `default`). Motion lives in ui.css keyed on
    // [data-slot="eyes-loader"] (+ .ui-eyesloader-pupil), reduced-motion gated.
    // Never splice a value into an arbitrary class — pick from fixed maps.
    $variant = $variant === 'default' ? 'default' : 'default';

    $eyes = [
        'sm' => 'size-5',
        'md' => 'size-7',
        'lg' => 'size-10',
    ];
    $pupils = [
        'sm' => 'size-2',
        'md' => 'size-2',
        'lg' => 'size-4',
    ];
    $eye = $eyes[$size] ?? $eyes['md'];
    $pupil = $pupils[$size] ?? $pupils['md'];

    $eyeClass = "ui-eyesloader-eye relative inline-flex shrink-0 rounded-full bg-card border border-border $eye";
    // While following, the pupil's position comes from --eye-dx/--eye-dy set by
    // eyes-loader.js (ui.css swaps the keyframes for that transform).
    $pupilClass = "ui-eyesloader-pupil absolute left-1/2 top-1/2 rounded-full bg-foreground $pupil";
@endphp

<span
    data-slot="eyes-loader"
    data-variant="{{ $variant }}"
    data-size="{{ $size }}"
    x-data="uiEyesLoader({ follow: {{ \Illuminate\Support\Js::from((bool) $follow) }} })"
    :data-following="following ? 'true' : null"
    role="status"
    aria-label="{{ __($label) }}"
    {{ $attributes->merge(['class' => 'inline-flex items-center gap-2 text-muted-foreground']) }}
>
    <span class="inline-flex items-center gap-1" aria-hidden="true">
        <span class="{{ $eyeClass }}" data-eye>
            <span class="{{ $pupilClass }}"></span>
        </span>
        <span class="{{ $eyeClass }}" data-eye>
            <span class="{{ $pupilClass }}"></span>
        </span>
    </span>
    <span class="sr-only">{{ __($label) }}</span>
</span>
resources/js/ui/eyes-loader.js JS
/**
 * Eyes loader behavior — the pupils follow the pointer.
 *
 * While a pointer moves anywhere on the page, each pupil looks towards it
 * (offset capped inside the eye); after 1.5s without movement the eyes go
 * back to the CSS wandering keyframes, so the loader never freezes into a
 * stare. Touch-only devices and reduced motion keep the CSS motion. `follow:
 * false` turns the tracking off. Listeners are removed on destroy.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiEyesLoader', (config = {}) => ({
        follow: config.follow ?? true,
        following: false,
        _onMove: null,
        _idle: null,
        _frame: null,

        init() {
            const fine = window.matchMedia('(pointer: fine)').matches;
            const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
            if (!this.follow || !fine || reduced) return;
            this._onMove = (e) => this.look(e.clientX, e.clientY);
            window.addEventListener('pointermove', this._onMove, { passive: true });
        },

        look(px, py) {
            cancelAnimationFrame(this._frame);
            this._frame = requestAnimationFrame(() => {
                this.following = true;
                this.$root.querySelectorAll('[data-eye]').forEach((eye) => {
                    const r = eye.getBoundingClientRect();
                    const cx = r.left + r.width / 2;
                    const cy = r.top + r.height / 2;
                    const dx = px - cx;
                    const dy = py - cy;
                    const distance = Math.hypot(dx, dy) || 1;
                    // The pupil travels at most 28% of the eye's radius from centre.
                    const reach = Math.min(distance, r.width * 0.28);
                    eye.style.setProperty('--eye-dx', `${(dx / distance) * reach}px`);
                    eye.style.setProperty('--eye-dy', `${(dy / distance) * reach}px`);
                });
            });
            clearTimeout(this._idle);
            this._idle = setTimeout(() => {
                this.following = false;
            }, 1500);
        },

        destroy() {
            if (this._onMove) window.removeEventListener('pointermove', this._onMove);
            clearTimeout(this._idle);
            cancelAnimationFrame(this._frame);
        },
    }));
});

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.2.1
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0