Arcade Pixel
An arcade display built from square cells. Text is rendered through a bundled 5x7
pixel font, and a sprite grid can take over when you need art instead of
letters: both are painted one fillRect per lit cell, with a single blurred
self-composite underneath for the phosphor bleed.
Every cell has a palette index, so a sprite can carry more than one colour. Index
0 is treated as background and skipped, which means the unlit dot matrix shows
through where the artwork is empty.
Usage
Pass text or sprite. The defaults are 8px cells with a 1px gap, a single
theme-primary ink and no animation.
<template>
<div class="flex w-full justify-center px-6 py-10">
<MArcadePixel
animate="type"
class="rounded-lg bg-zinc-950 p-6 ring-1 ring-white/10"
:glow="0.55"
loop
:pixel-size="12"
text="PLAY"
/>
</div>
</template>
The wrapper is inline-block and sized from the grid, so it hugs the message
without needing a fixed width.
Customize
animate picks the reveal: type lands the message a character at a time with a
white flash on each new column, wipe sweeps the full grid in, blink toggles
the display twice a second, and marquee scrolls the grid forever. speed
multiplies any of them, and loop repeats the finite ones.
palette is indexed by cell value, pixelSize and gap control the grid, and
columns clamps how many columns are visible at once. glow adds phosphor
bleed, scanlines overlays a scanline pattern, and crt adds a rolling band,
chromatic fringing, a vignette and a faint flicker.
A sprite grid, a two-colour palette, scanlines and the CRT overlay.
<template>
<div class="flex w-full flex-col items-center gap-5 px-6 py-10">
<MArcadePixel
animate="blink"
class="rounded-lg bg-zinc-950 p-8 ring-1 ring-white/10"
crt
:glow="0.75"
loop
:palette="['transparent', 'var(--ui-error)', 'var(--ui-warning)']"
:pixel-size="18"
scanlines
:sprite="heart"
/>
<p class="text-xs text-muted">
A sprite grid, a two-colour palette, scanlines and the CRT overlay.
</p>
</div>
</template>
<script setup lang="ts">
const heart = [
[0, 1, 1, 0, 0, 0, 1, 1, 0],
[1, 1, 1, 1, 0, 1, 1, 1, 1],
[1, 2, 1, 1, 1, 1, 1, 2, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
]
</script>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | - | Text rendered through the built-in 5x7 pixel font. Ignored when sprite is set. |
sprite | number[][] | - | Grid of palette indices. Takes precedence over text when both are set. |
palette | string[] | ['transparent', 'var(--ui-primary)'] | Colours indexed by cell value. Index 0 is background. |
animate | 'none' | 'type' | 'wipe' | 'blink' | 'marquee' | 'none' | Reveal animation played on mount. |
speed | number | 1 | Animation speed multiplier. Higher plays faster. |
loop | boolean | false | Whether the reveal animation repeats indefinitely. |
pixelSize | number | 8 | Size of a single pixel cell, in CSS pixels. |
gap | number | 1 | Gap between pixel cells, in CSS pixels. |
columns | number | - | Fixed display width in cells. Defaults to the content width. |
glow | boolean | number | false | Phosphor bloom bleeding out of lit pixels. true uses 0.65. |
scanlines | boolean | false | Overlays a CRT-style scanline pattern. |
crt | boolean | false | Adds a rolling band, chromatic fringing, a vignette and a faint flicker. |
class | string | - | Extra classes for the positioned wrapper. |
Accessibility
- The canvas is
aria-hiddenandpointer-events-none; the message is also rendered assr-onlytext so a screen reader announces the words rather than an empty box. - Every non-text value has a text equivalent: a sprite without
textannounces itself as "pixel art", so passtextalongside a sprite when the artwork carries meaning. - A
prefers-reduced-motionpreference paints the finished frame immediately and never starts the loop. The scanline drift, rolling band and flicker are CSS animations that stop under the same media query, so nothing moves without the reader's consent. - The display is decorative by default: the palette and glow never encode state, and colour is never the only signal.
Performance
- One
fillRectper lit cell, batched by colour, plus a single blurred self-composite for the glow. The glow costs one draw call, where a per-cellshadowBlurwould cost one blur per pixel. - The backing store is capped at 2x device pixel ratio and resized through a
ResizeObserver, so a wider container repaints at the right resolution without a re-render of the Vue tree. - The loop only repaints when the computed frame actually changes — a blinking display updates twice a second instead of sixty times.
- Scrolling out of view or hiding the tab cancels the animation frame, and the elapsed clock only advances while the loop is running, so a returning tab resumes instead of skipping ahead.