Pixel Loader <mv-pixel-loader>

Dot-matrix thinking indicators on a tiny pixel grid (3×3 to 9×9) in currentColor: snake, orbit, concentric ripple, scan, spiral, breathing, sparkle or diagonal wave, with an optional label crossed by a brighter band. The script only builds the grid; every motion is a CSS keyframe.

CategoryFeedback
TypeWeb Component (<mv-pixel-loader>)
Statusstable
Keywordsloader, spinner, pixel, dot-matrix, grid, ai, thinking, status, shimmer, css

When to use

  • An AI feature needs a distinctive thinking indicator with a shimmering label like Thinking
  • A loading state should feel techy and branded, with pattern choices such as snake, ripple or scan
  • A lightweight CSS-only status indicator is needed inside a chat bubble or toolbar

Avoid when

  • A neutral, conventional loader fits the product better, such as in a button or form → use Spinner instead
  • A voice assistant needs a presence that reacts to audio and conversation states → use Thinking Orb instead
  • The loading content has a predictable layout that could be previewed → use Skeleton instead

Install

node scripts/add.mjs pixel-loader --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["pixel-loader"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/pixel-loader/pixel-loader.js, components/pixel-loader/pixel-loader.css.

Usage

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<div id="mv-pxl-demo">
  <style>
    #mv-pxl-demo { display: grid; gap: 1.5rem; width: 100%; max-width: 44rem; }
    #mv-pxl-demo .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); gap: .75rem; margin: 0; padding: 0; list-style: none; }
    #mv-pxl-demo .grid li { display: grid; justify-items: center; gap: .875rem; padding: 1.25rem .75rem 1rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); }
    #mv-pxl-demo .grid code { font: 500 .75rem var(--mv-font-mono); color: var(--mv-fg-muted); }
    #mv-pxl-demo .chat { display: grid; gap: .875rem; padding: 1rem 1.125rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--mv-bg-subtle); font-size: .875rem; }
    #mv-pxl-demo .ask { justify-self: end; max-width: 80%; padding: .625rem .875rem; border-radius: var(--mv-radius-lg) var(--mv-radius-lg) var(--mv-radius-xs) var(--mv-radius-lg); background: var(--mv-accent); color: var(--mv-fg-on-accent); }
    #mv-pxl-demo .steps { display: flex; flex-wrap: wrap; gap: 1.25rem 1.75rem; padding-top: .75rem; border-top: 1px solid var(--mv-border); }
  </style>

  <ul class="grid" aria-label="Pixel loader variants">
    <li><mv-pixel-loader variant="snake" size="7" data-size="lg"></mv-pixel-loader><code>snake</code></li>
    <li><mv-pixel-loader variant="orbit" size="7" data-size="lg"></mv-pixel-loader><code>orbit</code></li>
    <li><mv-pixel-loader variant="ripple" size="7" data-size="lg"></mv-pixel-loader><code>ripple</code></li>
    <li><mv-pixel-loader variant="scan" size="7" data-size="lg"></mv-pixel-loader><code>scan</code></li>
    <li><mv-pixel-loader variant="spiral" size="7" data-size="lg"></mv-pixel-loader><code>spiral</code></li>
    <li><mv-pixel-loader variant="breathing" size="7" data-size="lg"></mv-pixel-loader><code>breathing</code></li>
    <li><mv-pixel-loader variant="sparkle" size="7" data-size="lg"></mv-pixel-loader><code>sparkle</code></li>
    <li><mv-pixel-loader variant="wave" size="7" data-size="lg"></mv-pixel-loader><code>wave</code></li>
  </ul>

  <div class="chat">
    <div class="ask">Which flights from Toronto to Lisbon land before noon on October 12?</div>
    <mv-pixel-loader variant="orbit" label="Thinking…" style="--mv-pixel-loader-color: var(--mv-accent)"></mv-pixel-loader>
    <div class="steps">
      <mv-pixel-loader variant="scan" label="Comparing 42 fares"></mv-pixel-loader>
      <mv-pixel-loader variant="wave" label="Checking layovers"></mv-pixel-loader>
      <mv-pixel-loader variant="sparkle" data-size="sm" label="Drafting the answer"></mv-pixel-loader>
    </div>
  </div>
</div>

API

Attributes

NameTypeDefaultDescription
variantsnake | orbit | ripple | scan | spiral | breathing | sparkle | wavesnakeAnimated pattern.
sizenumber5Grid side, from 3 to 9 cells.
labelstringVisible label crossed by a brighter band ("Thinking…"). Without a label, strings.loading ("Loading…") is still read by screen readers.
speednumber1Speed multiplier.
data-sizesm | lg | xlPixel size (3, 6 or 10 px; 4 px by default).

Properties

NameTypeDescription
stringsPartial<{ loading: string }>Overrides for the default text: loading ("Loading…"), read when no label is set. English defaults.

CSS classes

NameDescription
mv-pixel-loader-grid / -cell / -labelGenerated parts; each cell carries its moment in the cycle in --_t (idle cells have data-idle).

CSS variables

NameDefaultDescription
--mv-pixel-loader-colorvar(--mv-fg)Pixel color (otherwise currentColor via color).
--mv-pixel-loader-cell4pxSize of one pixel; the gap follows it unless set.
--mv-pixel-loader-gap0.375 × cellSpace between pixels.
--mv-pixel-loader-off0.14Opacity of an unlit pixel.
--mv-pixel-loader-radius1pxPixel corner radius (50% for round dots).
--mv-pixel-loader-label-color / -shineLabel color and the color of its moving band.

Accessibility

role="status" (polite live region) with a text label, visible or visually hidden; the grid is aria-hidden. Reduced motion (OS setting or data-motion="reduce"): the keyframes stop and the grid shows a still frame of its pattern, and the label band stops. Forced colors: pixels in CanvasText, plain label text.