drag · click · ← →
Compares a before and an after image with a line you drag between them.
A picture that resolves from blocks as it scrolls into view: a grid of squares in one colour lies over the slot, and each square clears on its own stretch of the picture's entry, picked by a seed, so the picture comes through in a scatter and stands whole by the time it is a third of the way up the screen. The squares run on the picture's own view timeline, so there is no script and no observer. Where view timelines are not supported, and with reduced motion, the picture is shown whole and the squares are never drawn. Zero JavaScript.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 4.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 115 · not Firefox · Safari 26; needs scroll-timeline. Elsewhere: without view timelines the picture is shown whole and no square is drawn.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import PixelReveal from '@moonarc/core/PixelReveal';
---
<PixelReveal cols={14} rows={9} seed="garden">
<img src="/garden.webp" alt="The garden at dusk, lanterns in the trees" width="1600" height="1000" />
</PixelReveal>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/pixel-reveal.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* PixelReveal — a picture that resolves from blocks as it scrolls into view: a grid of squares in one colour lies over
* the slot, and each square clears on its own stretch of the picture's entry, picked by a seed, so the picture comes
* through in a scatter and stands whole by the time it is a third of the way up the screen. The squares run on the
* picture's own view timeline: no script, no observer. Where view timelines are not supported, and with reduced
* motion (the media query, or a page's data-ma-motion="reduce"), the picture is shown whole and the squares are never
* drawn. Zero JavaScript.
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'div'> {
/** Squares across, 4 to 24. */
cols?: number;
/** Squares down, 3 to 18. */
rows?: number;
/** The squares' colour, any CSS colour. Unset, the block's ground, else the page's. */
colour?: string;
/** A word the scatter is varied by. */
seed?: string;
}
const { cols = 12, rows = 8, colour, seed = '', class: className, style, ...rest } = Astro.props;
if (cols < 4 || cols > 24 || rows < 3 || rows > 18) throw new Error(`PixelReveal: ${cols} by ${rows} squares; it takes 4 to 24 across and 3 to 18 down.`);
let h = 2166136261;
for (const c of `${cols}x${rows}:${seed}`) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
const r = () => ((h = Math.imul(h ^ (h >>> 15), 2246822507) + 0x6d2b79f5) >>> 0) / 4294967296;
// each square's start along the entry, 0 to 60 %: a scatter, a little earlier towards the bottom, which arrives first
const cells = Array.from({ length: cols * rows }, (_, i) => Math.round((r() * 0.75 + (1 - Math.floor(i / cols) / (rows - 1)) * 0.25) * 60));
const vars = [`--ma-px-cols:${cols}`, `--ma-px-rows:${rows}`, colour ? `--ma-px-colour:${colour}` : '', typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<div class:list={['ma-pixel', className]} style={vars} {...rest}>
<div class="ma-pixel__media"><slot /></div>
<div class="ma-pixel__cells" aria-hidden="true">{cells.map((s) => <i style={`--s:${s}%`} />)}</div>
</div>
<style is:global>
@layer components {
:where(.ma-pixel) { position: relative; display: grid; overflow: hidden; }
:where(.ma-pixel__media) { grid-area: 1 / 1; min-inline-size: 0; }
:where(.ma-pixel__media > *) { display: block; inline-size: 100%; block-size: 100%; }
:where(.ma-pixel__cells) { display: none; }
@supports (animation-timeline: view()) {
:where(.ma-pixel__cells) {
display: grid;
grid-area: 1 / 1;
grid-template-columns: repeat(var(--ma-px-cols), 1fr);
grid-template-rows: repeat(var(--ma-px-rows), 1fr);
pointer-events: none;
view-timeline: --ma-px block;
}
/* a hair of overlap, so no seam of the picture shows between two squares that have not cleared */
:where(.ma-pixel__cells > i) { margin: -0.5px; background: var(--ma-px-colour, var(--ma-block-ground, var(--background, Canvas))); animation: ma-px-clear linear both; }
/* the timeline in a rule of its own: in the same rule as the shorthand, a minifier folds it into the shorthand,
which does not take one, and the browser drops the declaration */
:where(html) :where(.ma-pixel__cells > i) { animation-timeline: --ma-px; animation-range: entry var(--s) entry calc(var(--s) + 40%); }
}
/* reduced motion (the media query, or the site's data-ma-motion switch): the picture whole, no squares */
@media (prefers-reduced-motion: reduce) {
:where(.ma-pixel__cells) { display: none; }
}
@keyframes ma-px-clear {
to { opacity: 0; }
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
cols | number | 12 | Squares across, 4 to 24. |
rows | number | 8 | Squares down, 3 to 18. |
colour | string | none | The squares' colour. Unset, the block's ground, else the page's. |
seed | string | '' | A word the scatter is varied by. |
The squares are not drawn: the picture is there whole.
ClientRouterNo script: it works on any page however it was reached.
Replaces: Pixel Transition (React Bits) · Pixelated image reveal (Magic UI). See the migration table.
drag · click · ← →
Compares a before and an after image with a line you drag between them.
A room drawn flat, for a stay that has no photograph of it yet: a single, a double, a twin, a suite, a family room, a bunk room or a loft, in the colours of the block or tone it sits on, with a shade, a light and a pale tone mixed from them.
Entrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown