Skip to content
Motion Atlas

Live examples of what AI can make for a website, each with the prompt that asks for it and the source to copy.

All examples / scroll story

Reveal on entry: feature rows shown by an IntersectionObserver

The same five feature rows as the scroll reveal, but a tiny script marks each row as it first enters the view and CSS transitions fade it in, so it works in every browser.

Prompt tested

Tamberlow · Features

Bookkeeping that keeps up with a busy stall

Five things that happen while you serve customers. Scroll on to see them.

  • Receipts in one snap

    Photograph a receipt and the amount, date and shop are filed for you.

  • Bank payments sort themselves

    Each payment lands in the right category before you open the app.

  • Tax totals, ready each quarter

    The figures your accountant asks for, added up as you go.

  • Polite nudges for late invoices

    One friendly reminder goes out a week after the due date, never more.

  • Locked with your fingerprint

    Every figure stays on your phone until you choose to share it.

Scroll inside ↓

Prompt

Build a features section whose rows fade in and slide up once as each one enters the view, using a small IntersectionObserver script and CSS transitions so it works in every browser (no scroll event listeners, no animation libraries, no Web Animations calls), as one HTML fragment, CSS scoped under one wrapper class, and a script module that exports mount(root) returning { play, pause }. Layout: a centred column (max-width 40rem, 1.25rem side padding) on a paper background #F6F4EF with ink text #1F2A44, in the system sans-serif. Invent the product: Tamberlow, a bookkeeping app for market stalls. First a header block at least 60cqh tall with its content vertically centred: a small uppercase slate (#5B6B8C) kicker 'Tamberlow · Features', a Georgia serif h2 headline (1.75rem) and a slate lede line, so the rows start below the first screen. Then a plain list of five feature rows with 1rem gaps: each row is a white card (1px #E8ECF3 border, 0.9rem radius, a faint shadow, 1rem padding) laid out as a grid with a 2.75rem rounded-square icon tile on the left and, on the right, an h3 title (about 1.05rem) over one slate line of about 12 words. The tiles cycle sky #7FB6FF, mint #6FD6B4, sun-yellow #FFC861, sky, mint, and each holds a simple line icon drawn as inline SVG in ink with round caps (receipt, bank, bar chart, bell, padlock). Leave 30cqh of space after the list. The script: unless prefers-reduced-motion is set (or there is no IntersectionObserver), mount() adds a data-ma-armed attribute to the wrapper and observes each row (threshold 0.15); when a row intersects it gets a data-ma-shown attribute and is unobserved, so it reveals only once; any earlier row not yet shown is shown with it, so a jump down the page never leaves a row hidden above. pause() disconnects the observer and removes data-ma-armed; play() arms again and observes the rows not yet shown. The CSS: the base styles are the finished list with every row visible; only [data-ma-armed] rows without [data-ma-shown] are hidden (opacity 0, translateY(24px)), with no transition on that hidden state, so arming hides them at once. Rows with [data-ma-shown] get transition-property: opacity, transform, 0.6s, ease-out; rows that enter in the same observer callback get an increasing index in a custom property that sets transition-delay to index × 80ms. Use the transition longhands, never transition: all. Add a prefers-reduced-motion rule that sets transition: none and animation: none. Use cqh for heights, no vh units, no position: fixed and no overflow: hidden or auto. With no script, every row shows.

Words it rests on

  • reveal on scroll with an IntersectionObserver (works in every browser)
  • the script only sets attributes: data-ma-armed on the wrapper, data-ma-shown on a row that enters
  • reveal once: unobserve each row after it is shown
  • show any earlier, skipped row together with the one that enters, so a jump never leaves a row hidden above
  • CSS transitions do the motion: transition-property: opacity, transform; 0.6s ease-out
  • transition on the shown state only, so arming hides rows at once
  • 80 ms stagger: transition-delay from an index custom property for rows that enter together
  • hidden only while armed, so no script, reduced motion or pause shows every row
  • mount(root) returning { play, pause }; pause() disconnects and disarms
  • no scroll event listeners
  • prefers-reduced-motion: no observer and transition none

Prompt test

A fresh AI session given only this prompt rebuilt this on 2026-11-05 using claude-opus-5-5; a separate fresh session using claude-opus-5-5 judged it “like this”.

The original example
Original
The rebuild made from the prompt alone
Rebuilt from the prompt

Lesson: For a reveal that works in every browser, describing the script as attributes only (`data-ma-armed` on the wrapper, `data-ma-shown` on a row, unobserve after showing), hiding rows only while armed, and putting the transition on the shown state only got a rebuild with the same once-per-row reveal, the 80 ms stagger and a fully readable list without the observer, on the first judged try. Say "show any earlier row a jump skipped", or a row passed over by a jump stays hidden. The observer margin was left out of the prompt and the reveal point drifted with it.

Facts

Tags
scroll story intersection observer css transitions editorial harbor
Weight
2.7 KB gzip (HTML + CSS + JS)
Needs
No library; a small JavaScript module
Browser support
Every browser: IntersectionObserver is in Chrome 58+, Edge 16+, Firefox 55+ and Safari 12.1+ (MDN). Without it, without script or with reduced motion, every row shows in place.
Motion
follows the scroll; without support or with reduced motion the finished layout shows
Link
/examples/reveal-observer/

Source

Paste all three parts into any page. Wrap the HTML in <div class="ma-reveal-observer"> and put the CSS in a <style> element or your stylesheet; every class, id and animation name starts with ma-reveal-observer, so it will not clash with your styles.

demo.html

<section class="ma-reveal-observer-section">
  <header class="ma-reveal-observer-head">
    <p class="ma-reveal-observer-kicker">Tamberlow · Features</p>
    <h2>Bookkeeping that keeps up with a busy stall</h2>
    <p class="ma-reveal-observer-lede">Five things that happen while you serve customers. Scroll on to see them.</p>
  </header>
  <ul class="ma-reveal-observer-list">
    <li class="ma-reveal-observer-row">
      <span class="ma-reveal-observer-icon ma-reveal-observer-sky" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 3h10v18l-2.5-1.5L12 21l-2.5-1.5L7 21z M9.5 8h5 M9.5 12h5"/></svg></span>
      <div><h3>Receipts in one snap</h3><p>Photograph a receipt and the amount, date and shop are filed for you.</p></div>
    </li>
    <li class="ma-reveal-observer-row">
      <span class="ma-reveal-observer-icon ma-reveal-observer-mint" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 9l8-5 8 5 M6 10v7 M10 10v7 M14 10v7 M18 10v7 M4 19.5h16"/></svg></span>
      <div><h3>Bank payments sort themselves</h3><p>Each payment lands in the right category before you open the app.</p></div>
    </li>
    <li class="ma-reveal-observer-row">
      <span class="ma-reveal-observer-icon ma-reveal-observer-sun" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 20h16 M7 17v-5 M12 17V7 M17 17V9"/></svg></span>
      <div><h3>Tax totals, ready each quarter</h3><p>The figures your accountant asks for, added up as you go.</p></div>
    </li>
    <li class="ma-reveal-observer-row">
      <span class="ma-reveal-observer-icon ma-reveal-observer-sky" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z M10 20.5h4"/></svg></span>
      <div><h3>Polite nudges for late invoices</h3><p>One friendly reminder goes out a week after the due date, never more.</p></div>
    </li>
    <li class="ma-reveal-observer-row">
      <span class="ma-reveal-observer-icon ma-reveal-observer-mint" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 11h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2z M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>
      <div><h3>Locked with your fingerprint</h3><p>Every figure stays on your phone until you choose to share it.</p></div>
    </li>
  </ul>
</section>

demo.css

/* Reveal on entry with an IntersectionObserver: the same five feature rows, shown by a small
   script and CSS transitions, so it works in every browser. The base styles are the finished
   list. Rows hide only while the script has armed the section ([data-ma-armed]) and they have
   not been shown yet; with no script, no observer, reduced motion or pause, every row is in
   place. The transition sits on the shown state only, so arming hides rows at once and showing
   them fades them in (opacity and transform only), each about 80 ms after the one before. */
.ma-reveal-observer {
  background: #F6F4EF;
  color: #1F2A44;
}

.ma-reveal-observer .ma-reveal-observer-section {
  max-width: 40rem;
  margin: 0 auto;
  padding: 0 1.25rem 30cqh;
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 1rem;
  line-height: 1.5;
}

.ma-reveal-observer .ma-reveal-observer-head {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 60cqh;
  padding: 2rem 0;
}

.ma-reveal-observer .ma-reveal-observer-kicker {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #5B6B8C;
}

.ma-reveal-observer h2 {
  margin: 0 0 0.75rem;
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 1.75rem;
  line-height: 1.2;
}

.ma-reveal-observer .ma-reveal-observer-lede {
  margin: 0;
  font-size: 1.05rem;
  color: #5B6B8C;
}

.ma-reveal-observer .ma-reveal-observer-list {
  display: grid;
  gap: 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ma-reveal-observer .ma-reveal-observer-row {
  display: grid;
  grid-template-columns: 2.75rem 1fr;
  gap: 1rem;
  align-items: start;
  padding: 1rem 1.1rem;
  border: 1px solid #E8ECF3;
  border-radius: 0.9rem;
  background: #FFFFFF;
  box-shadow: 0 1px 2px rgba(31, 42, 68, 0.06);
}

.ma-reveal-observer .ma-reveal-observer-icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 0.75rem;
}

.ma-reveal-observer .ma-reveal-observer-sky { background: #7FB6FF; }
.ma-reveal-observer .ma-reveal-observer-mint { background: #6FD6B4; }
.ma-reveal-observer .ma-reveal-observer-sun { background: #FFC861; }

.ma-reveal-observer svg {
  width: 1.5rem;
  height: 1.5rem;
  fill: none;
  stroke: #1F2A44;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ma-reveal-observer h3 {
  margin: 0.1rem 0 0.25rem;
  font-size: 1.05rem;
  line-height: 1.3;
}

.ma-reveal-observer .ma-reveal-observer-row p {
  margin: 0;
  color: #5B6B8C;
}

/* Armed by the script: rows not shown yet wait hidden and 24 px low. */
.ma-reveal-observer[data-ma-armed] .ma-reveal-observer-row:not([data-ma-shown]) {
  opacity: 0;
  transform: translateY(24px);
}

/* Shown: fade and rise into place; rows that enter together follow each other by 80 ms. */
.ma-reveal-observer .ma-reveal-observer-row[data-ma-shown] {
  transition-property: opacity, transform;
  transition-duration: 0.6s;
  transition-timing-function: ease-out;
  transition-delay: calc(var(--ma-reveal-observer-order, 0) * 80ms);
}

@media (prefers-reduced-motion: reduce) {
  .ma-reveal-observer,
  .ma-reveal-observer * {
    animation: none !important;
    transition: none !important;
  }
}

The motion lives in a small module. Load it with <script type="module"> and start it with mount(document.querySelector('.ma-reveal-observer')); it returns { play, pause } and starts nothing when the visitor prefers reduced motion.

demo.js

// Reveal on entry: an IntersectionObserver marks each feature row with data-ma-shown the first
// time it enters the view (and any earlier row a jump skipped), and CSS transitions do the fade
// and rise. The script only sets attributes; it never animates and never listens to the scroll.
// Under prefers-reduced-motion, or where there is no IntersectionObserver, it does nothing and
// every row stays in place.
export function mount(root, options = {}) {
  const mm = options.matchMedia ?? globalThis.matchMedia;
  const reduced = options.reduced ?? (mm ? mm('(prefers-reduced-motion: reduce)').matches : false);
  const Observer = 'IntersectionObserver' in options ? options.IntersectionObserver : globalThis.IntersectionObserver;
  const rows = [...root.querySelectorAll('.ma-reveal-observer-row')];
  let io;

  const show = (row, order) => {
    // Rows shown together get a growing order, which CSS turns into an 80 ms stagger.
    row.style.setProperty('--ma-reveal-observer-order', String(order));
    row.setAttribute('data-ma-shown', '');
    io?.unobserve(row);
  };

  const reveal = (entries) => {
    let last = -1;
    for (const e of entries) if (e.isIntersecting) last = Math.max(last, rows.indexOf(e.target));
    // Show the entering rows and any earlier row a jump skipped, so none stays hidden above.
    let order = 0;
    for (const row of rows.slice(0, last + 1)) if (!row.hasAttribute('data-ma-shown')) show(row, order++);
  };

  const play = () => {
    if (reduced || !Observer || io) return;
    io = new Observer(reveal, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });
    root.setAttribute('data-ma-armed', '');
    for (const row of rows) if (!row.hasAttribute('data-ma-shown')) io.observe(row);
  };

  // Paused (off screen or pause-all): stop observing and disarm, so every row shows.
  const pause = () => {
    io?.disconnect();
    io = undefined;
    root.removeAttribute('data-ma-armed');
  };

  play();
  return { play, pause };
}

Same idea, other ways

Say 'IntersectionObserver adds an attribute, CSS transitions animate it' for a version that works in every browser and plays each row once on entry, at its own pace.

See all 2 side by side →