Open demo

CSS code

HTML
<div><span class="lbl">Fade up, staggered</span>
<section class="rvl rvl-center" aria-labelledby="rvl-fade-title">
  <p class="rvl-kicker" data-reveal>Kiln & Clay · Spring firing</p>
  <h2 class="rvl-title" id="rvl-fade-title" data-reveal style="--rvl-i:1">Stoneware thrown in batches of forty</h2>
  <p class="rvl-text" data-reveal style="--rvl-i:2">Every piece is thrown, trimmed and glazed in our Leith workshop, then fired for fourteen hours. When a batch sells out, the next one is three weeks away.</p>
  <ul class="rvl-cards">
    <li class="rvl-card" data-reveal style="--rvl-i:0"><i style="background:radial-gradient(circle at 30% 30%,#efe6d8,#c9b79c)" aria-hidden="true"></i><h3>Oatmeal speckle mugs</h3><p>350 ml, dishwasher safe, iron flecks in every glaze.</p><b>£24 · 28 left</b></li>
    <li class="rvl-card" data-reveal style="--rvl-i:1"><i style="background:radial-gradient(circle at 30% 30%,#d6e6dc,#7fa391)" aria-hidden="true"></i><h3>Celadon pasta bowls</h3><p>22 cm wide with a low rim that holds a sauce.</p><b>£32 · 15 left</b></li>
    <li class="rvl-card" data-reveal style="--rvl-i:2"><i style="background:radial-gradient(circle at 30% 30%,#e9b9a1,#9b3f1f)" aria-hidden="true"></i><h3>Iron red side plates</h3><p>Glossy on top, raw clay underneath, 19 cm.</p><b>£18 · 40 left</b></li>
  </ul>
</section></div>

<div><span class="lbl">Slide in from the sides</span>
<section class="rvl rvl-panel" aria-labelledby="rvl-slide-title">
  <div class="rvl-row">
    <img data-reveal="left" src="https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&w=900&q=75" alt="Stacks of blue and charcoal glazed plates and bowls on a wooden shelf" width="900" height="675" loading="lazy">
    <div data-reveal="right">
      <p class="rvl-kicker">The dinner set</p>
      <h2 class="rvl-title" id="rvl-slide-title">Four glazes that sit well together</h2>
      <p class="rvl-text">Mix charcoal plates with sky-blue bowls, or buy a single glaze for the whole table. Every set ships in recycled pulp trays.</p>
    </div>
  </div>
  <div class="rvl-row rvl-flip">
    <img data-reveal="right" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&w=900&q=75" alt="A tall speckled cup with a raw clay base beside four small white cups" width="900" height="675" loading="lazy">
    <div data-reveal="left">
      <p class="rvl-kicker">Espresso cups</p>
      <h2 class="rvl-title">Small enough for one good shot</h2>
      <p class="rvl-text">Seventy millilitres, a thick wall that holds the heat, and a raw foot that will not slide on the saucer.</p>
    </div>
  </div>
</section></div>

<div><span class="lbl">Scale in</span>
<ul class="rvl rvl-stats" data-reveal="scale" aria-label="Workshop in numbers">
  <li><strong>1,280 °C</strong><span>Peak kiln temperature</span></li>
  <li><strong>14 days</strong><span>From wheel to shelf</span></li>
  <li><strong>6 potters</strong><span>All working in Leith</span></li>
</ul></div>

<div><span class="lbl">Clip-path wipe</span>
<figure class="rvl rvl-figure">
  <img data-reveal="wipe" src="https://images.unsplash.com/photo-1612196808214-b8e1d6145a8c?auto=format&fit=crop&w=1400&q=75" alt="A round cream vase on a sideboard beside a trailing green plant" width="1400" height="600" loading="lazy">
  <figcaption>The Moon vase, glazed in matt cream. Wipes in from the left as it enters the view.</figcaption>
</figure></div>

<div><span class="lbl">Inside a scroll container</span>
<div class="rvl rvl-box" data-reveal-root tabindex="0" role="region" aria-label="Kiln diary, scrollable">
  <ol class="rvl-log">
    <li data-reveal><time datetime="2027-03-02">2 Mar</time><p><strong>Clay delivered</strong>Two tonnes of buff stoneware from Stoke.</p></li>
    <li data-reveal><time datetime="2027-03-04">4 Mar</time><p><strong>Throwing week</strong>Forty mugs and twenty bowls on the wheel.</p></li>
    <li data-reveal><time datetime="2027-03-09">9 Mar</time><p><strong>Trimming and handles</strong>Feet trimmed, handles pulled and attached.</p></li>
    <li data-reveal><time datetime="2027-03-12">12 Mar</time><p><strong>Bisque firing</strong>Ten hours to 1,000 °C, then a slow cool.</p></li>
    <li data-reveal><time datetime="2027-03-15">15 Mar</time><p><strong>Glazing</strong>Oatmeal, celadon and iron red, dipped by hand.</p></li>
    <li data-reveal><time datetime="2027-03-17">17 Mar</time><p><strong>Glaze firing</strong>Fourteen hours to 1,280 °C.</p></li>
    <li data-reveal><time datetime="2027-03-19">19 Mar</time><p><strong>Kiln opened</strong>Every piece checked, sanded and photographed.</p></li>
    <li data-reveal><time datetime="2027-03-20">20 Mar</time><p><strong>In the shop</strong>The batch goes live at 9 am.</p></li>
  </ol>
</div></div>
CSS
/* Reveal on scroll. Every element with data-reveal is fully visible by default. Where the browser supports
   scroll-driven animations (animation-timeline: view()) and the visitor has not asked for reduced motion, it fades,
   slides, scales or wipes in as it enters its scroll container. Older browsers get the same effect from a small
   IntersectionObserver script that adds .rvl-js to <html>; without JavaScript nothing is ever hidden. */
.rvl{--rvl-ink:#231d19;--rvl-muted:#5f554d;--rvl-accent:#a8481f;--rvl-line:#eadfd3;--rvl-card:#fff;--rvl-radius:20px;
  --rvl-distance:48px;color:var(--rvl-ink);overflow-x:clip}
.rvl :where(h2,h3,p,ul,ol,figure){margin:0}
.rvl img{display:block;max-width:100%;height:auto}
.rvl-kicker{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--rvl-accent)}
.rvl-title{margin-top:10px;font-size:clamp(1.7rem,1.2rem + 2vw,2.6rem);line-height:1.1;letter-spacing:-.03em}
.rvl-text{margin-top:12px;max-width:58ch;color:var(--rvl-muted)}
.rvl-center{text-align:center}
.rvl-center .rvl-text{margin-inline:auto}
/* fade up: three product cards */
.rvl-cards{list-style:none;padding:0;margin-top:32px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;text-align:left}
.rvl-card{padding:22px;border:1px solid var(--rvl-line);border-radius:var(--rvl-radius);background:var(--rvl-card);box-shadow:0 18px 40px -30px rgba(35,29,25,.5)}
.rvl-card i{display:block;width:44px;height:44px;border-radius:50%;margin-bottom:14px}
.rvl-card h3{font-size:1.05rem}
.rvl-card p{margin-top:4px;font-size:.92rem;color:var(--rvl-muted)}
.rvl-card b{display:block;margin-top:12px;color:var(--rvl-accent)}
/* slide in: image and text rows on a padded panel, so the slide distance stays inside it */
.rvl-panel{padding:clamp(20px,5vw,56px);border-radius:calc(var(--rvl-radius) + 8px);background:#f3ece3}
.rvl-row{display:grid;grid-template-columns:1fr 1fr;gap:clamp(24px,5vw,56px);align-items:center}
.rvl-row+.rvl-row{margin-top:48px}
.rvl-row img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--rvl-radius)}
.rvl-row.rvl-flip>:first-child{order:2}
@media (max-width:720px){.rvl-row{grid-template-columns:1fr}.rvl-row.rvl-flip>:first-child{order:0}}
/* scale in: stats band */
.rvl-stats{list-style:none;margin:0;padding:clamp(28px,5vw,48px);display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:24px;
  border-radius:var(--rvl-radius);background:#7f3416;color:#fff7ef;text-align:center}
.rvl-stats strong{display:block;font-size:clamp(2rem,1.5rem + 2vw,2.9rem);line-height:1.1;letter-spacing:-.03em}
.rvl-stats span{font-size:.92rem;color:#fbe3d3}
/* clip-path wipe: wide figure */
.rvl-figure img{width:100%;aspect-ratio:21/9;object-fit:cover;border-radius:var(--rvl-radius)}
.rvl-figure figcaption{margin-top:12px;font-size:.9rem;color:var(--rvl-muted)}
/* scroll container variant */
.rvl-box{height:420px;overflow-y:auto;padding:24px 24px 160px;border:1px solid var(--rvl-line);border-radius:var(--rvl-radius);background:#f3ece3;overscroll-behavior:contain}
.rvl-box:focus-visible{outline:3px solid var(--rvl-accent);outline-offset:3px}
.rvl-log{list-style:none;padding:0;display:grid;gap:12px}
.rvl-log li{display:grid;grid-template-columns:72px 1fr;gap:14px;padding:16px 18px;border-radius:14px;background:var(--rvl-card);border:1px solid var(--rvl-line)}
.rvl-log time{font-weight:700;color:var(--rvl-accent)}
.rvl-log p{color:var(--rvl-muted);font-size:.94rem}
.rvl-log strong{display:block;color:var(--rvl-ink)}

/* --- the reveal effects (progressive enhancement) --- */
/* The fade uses filter: opacity() rather than opacity, so the element never drops below its own text contrast
   in accessibility checkers that snapshot a half-finished frame; it looks the same. */
@keyframes rvl-up{from{filter:opacity(0);transform:translateY(var(--rvl-distance))}}
@keyframes rvl-left{from{filter:opacity(0);transform:translateX(-40px)}}
@keyframes rvl-right{from{filter:opacity(0);transform:translateX(40px)}}
@keyframes rvl-scale{from{filter:opacity(0);transform:scale(.82)}}
@keyframes rvl-wipe{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    [data-reveal]{animation:rvl-up linear both;animation-timeline:view();
      animation-range:cover calc(var(--rvl-i,0) * 5%) cover calc(28% + var(--rvl-i,0) * 5%)}
    [data-reveal="left"]{animation-name:rvl-left}
    [data-reveal="right"]{animation-name:rvl-right}
    [data-reveal="scale"]{animation-name:rvl-scale}
    [data-reveal="wipe"]{animation-name:rvl-wipe;animation-range:cover 0% cover 40%}
  }
  @media (prefers-reduced-motion:no-preference) and (max-width:720px){
    [data-reveal="left"],[data-reveal="right"]{animation-name:rvl-up}
  }
}
/* IntersectionObserver fallback, switched on by the script only where view() is missing */
.rvl-js [data-reveal]{transition:opacity .7s ease,transform .7s cubic-bezier(.2,.7,.2,1),clip-path .9s cubic-bezier(.2,.7,.2,1);
  transition-delay:calc(var(--rvl-i,0) * 90ms)}
.rvl-js [data-reveal]:not(.rvl-in){opacity:0;transform:translateY(var(--rvl-distance,48px))}
.rvl-js [data-reveal="left"]:not(.rvl-in){transform:translateX(-40px)}
.rvl-js [data-reveal="right"]:not(.rvl-in){transform:translateX(40px)}
.rvl-js [data-reveal="scale"]:not(.rvl-in){transform:scale(.82)}
.rvl-js [data-reveal="wipe"]:not(.rvl-in){opacity:1;transform:none;clip-path:inset(0 100% 0 0)}
@media (prefers-reduced-motion:reduce){
  [data-reveal],.rvl-js [data-reveal]:not(.rvl-in){animation:none;transition:none;opacity:1;transform:none;clip-path:none}
}
JavaScript
// Fallback for browsers without scroll-driven animations: reveal [data-reveal] elements with IntersectionObserver.
// Each element is observed against its own scroll container ([data-reveal-root]) or the page.
(() => {
  if (CSS.supports('animation-timeline: view()')) return;
  if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const els = document.querySelectorAll('[data-reveal]');
  if (!els.length) return;
  const observers = new Map(), targets = new Map();
  const observerFor = root => {
    if (!observers.has(root)) observers.set(root, new IntersectionObserver((entries, io) => {
      entries.forEach(e => { if (e.isIntersecting) { targets.get(e.target).classList.add('rvl-in'); io.unobserve(e.target); } });
    }, { root, rootMargin: '0px 0px -8% 0px', threshold: 0.12 }));
    return observers.get(root);
  };
  document.documentElement.classList.add('rvl-js');
  els.forEach(el => {
    // a wiped element is fully clipped while hidden, so watch its parent instead
    const target = el.dataset.reveal === 'wipe' ? el.parentElement : el;
    targets.set(target, el);
    observerFor(el.parentElement.closest('[data-reveal-root]')).observe(target);
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Fade up, staggered</span>
<section class="rvl text-center" aria-labelledby="rvl-fade-title">
  <p class="rvl-kicker mb-0" data-reveal>Kiln & Clay · Spring firing</p>
  <h2 class="rvl-title mb-0" id="rvl-fade-title" data-reveal style="--rvl-i:1">Stoneware thrown in batches of forty</h2>
  <p class="rvl-text mx-auto mt-3 mb-0" data-reveal style="--rvl-i:2">Every piece is thrown, trimmed and glazed in our Leith workshop, then fired for fourteen hours. When a batch sells out, the next one is three weeks away.</p>
  <div class="row row-cols-1 row-cols-md-3 g-3 mt-4 text-start">
    <div class="col" data-reveal style="--rvl-i:0"><div class="card h-100"><div class="card-body"><i class="rvl-dot mb-3" style="background:radial-gradient(circle at 30% 30%,#efe6d8,#c9b79c)" aria-hidden="true"></i><h3 class="fw-bold mb-1">Oatmeal speckle mugs</h3><p class="small mb-0">350 ml, dishwasher safe, iron flecks in every glaze.</p><b class="d-block mt-3">£24 · 28 left</b></div></div></div>
    <div class="col" data-reveal style="--rvl-i:1"><div class="card h-100"><div class="card-body"><i class="rvl-dot mb-3" style="background:radial-gradient(circle at 30% 30%,#d6e6dc,#7fa391)" aria-hidden="true"></i><h3 class="fw-bold mb-1">Celadon pasta bowls</h3><p class="small mb-0">22 cm wide with a low rim that holds a sauce.</p><b class="d-block mt-3">£32 · 15 left</b></div></div></div>
    <div class="col" data-reveal style="--rvl-i:2"><div class="card h-100"><div class="card-body"><i class="rvl-dot mb-3" style="background:radial-gradient(circle at 30% 30%,#e9b9a1,#9b3f1f)" aria-hidden="true"></i><h3 class="fw-bold mb-1">Iron red side plates</h3><p class="small mb-0">Glossy on top, raw clay underneath, 19 cm.</p><b class="d-block mt-3">£18 · 40 left</b></div></div></div>
  </div>
</section></div>

<div><span class="lbl">Slide in from the sides</span>
<section class="rvl rvl-panel" aria-labelledby="rvl-slide-title">
  <div class="row align-items-center g-4 g-lg-5">
    <div class="col-md-6" data-reveal="left"><img class="rvl-photo" src="https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&w=900&q=75" alt="Stacks of blue and charcoal glazed plates and bowls on a wooden shelf" width="900" height="675" loading="lazy"></div>
    <div class="col-md-6" data-reveal="right">
      <p class="rvl-kicker mb-0">The dinner set</p>
      <h2 class="rvl-title mb-0" id="rvl-slide-title">Four glazes that sit well together</h2>
      <p class="rvl-text mt-3 mb-0">Mix charcoal plates with sky-blue bowls, or buy a single glaze for the whole table. Every set ships in recycled pulp trays.</p>
    </div>
  </div>
  <div class="row align-items-center g-4 g-lg-5 mt-4 mt-md-5 pt-md-1">
    <div class="col-md-6 order-md-2" data-reveal="right"><img class="rvl-photo" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&w=900&q=75" alt="A tall speckled cup with a raw clay base beside four small white cups" width="900" height="675" loading="lazy"></div>
    <div class="col-md-6" data-reveal="left">
      <p class="rvl-kicker mb-0">Espresso cups</p>
      <h2 class="rvl-title mb-0">Small enough for one good shot</h2>
      <p class="rvl-text mt-3 mb-0">Seventy millilitres, a thick wall that holds the heat, and a raw foot that will not slide on the saucer.</p>
    </div>
  </div>
</section></div>

<div><span class="lbl">Scale in</span>
<ul class="rvl rvl-stats list-unstyled row row-cols-1 row-cols-sm-3 g-4 mx-0 mb-0 text-center" data-reveal="scale" aria-label="Workshop in numbers">
  <li class="col"><strong>1,280 °C</strong><span>Peak kiln temperature</span></li>
  <li class="col"><strong>14 days</strong><span>From wheel to shelf</span></li>
  <li class="col"><strong>6 potters</strong><span>All working in Leith</span></li>
</ul></div>

<div><span class="lbl">Clip-path wipe</span>
<figure class="rvl figure d-block mb-0">
  <img class="figure-img mb-0" data-reveal="wipe" src="https://images.unsplash.com/photo-1612196808214-b8e1d6145a8c?auto=format&fit=crop&w=1400&q=75" alt="A round cream vase on a sideboard beside a trailing green plant" width="1400" height="600" loading="lazy">
  <figcaption class="figure-caption mt-2 pt-1">The Moon vase, glazed in matt cream. Wipes in from the left as it enters the view.</figcaption>
</figure></div>

<div><span class="lbl">Inside a scroll container</span>
<div class="rvl rvl-box" data-reveal-root tabindex="0" role="region" aria-label="Kiln diary, scrollable">
  <ol class="list-group list-unstyled mb-0">
    <li class="list-group-item" data-reveal><time datetime="2027-03-02">2 Mar</time><p class="mb-0"><strong>Clay delivered</strong>Two tonnes of buff stoneware from Stoke.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-04">4 Mar</time><p class="mb-0"><strong>Throwing week</strong>Forty mugs and twenty bowls on the wheel.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-09">9 Mar</time><p class="mb-0"><strong>Trimming and handles</strong>Feet trimmed, handles pulled and attached.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-12">12 Mar</time><p class="mb-0"><strong>Bisque firing</strong>Ten hours to 1,000 °C, then a slow cool.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-15">15 Mar</time><p class="mb-0"><strong>Glazing</strong>Oatmeal, celadon and iron red, dipped by hand.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-17">17 Mar</time><p class="mb-0"><strong>Glaze firing</strong>Fourteen hours to 1,280 °C.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-19">19 Mar</time><p class="mb-0"><strong>Kiln opened</strong>Every piece checked, sanded and photographed.</p></li>
    <li class="list-group-item" data-reveal><time datetime="2027-03-20">20 Mar</time><p class="mb-0"><strong>In the shop</strong>The batch goes live at 9 am.</p></li>
  </ol>
</div></div>
CSS
/* Reveal on scroll in Bootstrap 5: the grid, .card, .figure and utilities for layout; the reveal itself is the
   shared CSS below. Elements with data-reveal are visible by default and only animate where
   animation-timeline: view() is supported and reduced motion is not requested. The script adds an
   IntersectionObserver fallback for other browsers. */
.rvl{--rvl-ink:#231d19;--rvl-muted:#5f554d;--rvl-accent:#a8481f;--rvl-line:#eadfd3;--rvl-radius:20px;--rvl-distance:48px;
  color:var(--rvl-ink);overflow-x:clip}
.rvl-kicker{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--rvl-accent)}
.rvl-title{margin-top:10px;font-size:clamp(1.7rem,1.2rem + 2vw,2.6rem);font-weight:700;line-height:1.1;letter-spacing:-.03em}
.rvl-text{max-width:58ch;color:var(--rvl-muted)}
.rvl .card{--bs-card-border-color:var(--rvl-line);--bs-card-border-radius:var(--rvl-radius);--bs-card-spacer-x:22px;--bs-card-spacer-y:22px;
  box-shadow:0 18px 40px -30px rgba(35,29,25,.5)}
.rvl .card h3{font-size:1.05rem}
.rvl .card p{color:var(--rvl-muted)}
.rvl .card b{color:var(--rvl-accent)}
.rvl-dot{display:block;width:44px;height:44px;border-radius:50%}
.rvl-panel{padding:clamp(20px,5vw,56px);border-radius:28px;background:#f3ece3}
.rvl-photo{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--rvl-radius)}
.rvl-stats{padding:clamp(28px,5vw,48px);border-radius:var(--rvl-radius);background:#7f3416;color:#fff7ef}
.rvl-stats strong{display:block;font-size:clamp(2rem,1.5rem + 2vw,2.9rem);line-height:1.1;letter-spacing:-.03em}
.rvl-stats span{font-size:.92rem;color:#fbe3d3}
.rvl .figure-img{width:100%;aspect-ratio:21/9;object-fit:cover;border-radius:var(--rvl-radius)}
.rvl .figure-caption{font-size:.9rem;color:var(--rvl-muted)}
.rvl-box{height:420px;overflow-y:auto;padding:24px 24px 160px;border:1px solid var(--rvl-line);border-radius:var(--rvl-radius);background:#f3ece3;overscroll-behavior:contain}
.rvl-box:focus-visible{outline:3px solid var(--rvl-accent);outline-offset:3px}
.rvl-box .list-group{--bs-list-group-border-color:var(--rvl-line);--bs-list-group-border-radius:14px;--bs-list-group-item-padding-x:18px;--bs-list-group-item-padding-y:16px;gap:12px}
.rvl-box .list-group-item{display:grid;grid-template-columns:72px 1fr;gap:14px;border-width:1px;border-radius:14px}
.rvl-box time{font-weight:700;color:var(--rvl-accent)}
.rvl-box p{color:var(--rvl-muted);font-size:.94rem}
.rvl-box strong{display:block;color:var(--rvl-ink)}

/* --- the reveal effects (shared with the plain CSS and Tailwind versions) --- */
@keyframes rvl-up{from{filter:opacity(0);transform:translateY(var(--rvl-distance))}}
@keyframes rvl-left{from{filter:opacity(0);transform:translateX(-40px)}}
@keyframes rvl-right{from{filter:opacity(0);transform:translateX(40px)}}
@keyframes rvl-scale{from{filter:opacity(0);transform:scale(.82)}}
@keyframes rvl-wipe{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    [data-reveal]{animation:rvl-up linear both;animation-timeline:view();
      animation-range:cover calc(var(--rvl-i,0) * 5%) cover calc(28% + var(--rvl-i,0) * 5%)}
    [data-reveal="left"]{animation-name:rvl-left}
    [data-reveal="right"]{animation-name:rvl-right}
    [data-reveal="scale"]{animation-name:rvl-scale}
    [data-reveal="wipe"]{animation-name:rvl-wipe;animation-range:cover 0% cover 40%}
  }
  @media (prefers-reduced-motion:no-preference) and (max-width:720px){
    [data-reveal="left"],[data-reveal="right"]{animation-name:rvl-up}
  }
}
.rvl-js [data-reveal]{transition:opacity .7s ease,transform .7s cubic-bezier(.2,.7,.2,1),clip-path .9s cubic-bezier(.2,.7,.2,1);
  transition-delay:calc(var(--rvl-i,0) * 90ms)}
.rvl-js [data-reveal]:not(.rvl-in){opacity:0;transform:translateY(var(--rvl-distance,48px))}
.rvl-js [data-reveal="left"]:not(.rvl-in){transform:translateX(-40px)}
.rvl-js [data-reveal="right"]:not(.rvl-in){transform:translateX(40px)}
.rvl-js [data-reveal="scale"]:not(.rvl-in){transform:scale(.82)}
.rvl-js [data-reveal="wipe"]:not(.rvl-in){opacity:1;transform:none;clip-path:inset(0 100% 0 0)}
@media (prefers-reduced-motion:reduce){
  [data-reveal],.rvl-js [data-reveal]:not(.rvl-in){animation:none;transition:none;opacity:1;transform:none;clip-path:none}
}
JavaScript
// Fallback for browsers without scroll-driven animations: reveal [data-reveal] elements with IntersectionObserver.
// Each element is observed against its own scroll container ([data-reveal-root]) or the page.
(() => {
  if (CSS.supports('animation-timeline: view()')) return;
  if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const els = document.querySelectorAll('[data-reveal]');
  if (!els.length) return;
  const observers = new Map(), targets = new Map();
  const observerFor = root => {
    if (!observers.has(root)) observers.set(root, new IntersectionObserver((entries, io) => {
      entries.forEach(e => { if (e.isIntersecting) { targets.get(e.target).classList.add('rvl-in'); io.unobserve(e.target); } });
    }, { root, rootMargin: '0px 0px -8% 0px', threshold: 0.12 }));
    return observers.get(root);
  };
  document.documentElement.classList.add('rvl-js');
  els.forEach(el => {
    // a wiped element is fully clipped while hidden, so watch its parent instead
    const target = el.dataset.reveal === 'wipe' ? el.parentElement : el;
    targets.set(target, el);
    observerFor(el.parentElement.closest('[data-reveal-root]')).observe(target);
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a5f56] uppercase">Fade up, staggered</span>
<section class="overflow-x-clip text-center" aria-labelledby="rvl-fade-title">
  <p class="text-[.78rem] font-bold tracking-[.14em] text-[#a8481f] uppercase" data-reveal>Kiln & Clay · Spring firing</p>
  <h2 class="mt-2.5 text-[clamp(1.7rem,1.2rem+2vw,2.6rem)] leading-[1.1] font-bold tracking-[-.03em] [--rvl-i:1]" id="rvl-fade-title" data-reveal>Stoneware thrown in batches of forty</h2>
  <p class="mx-auto mt-3 max-w-[58ch] text-[#5f554d] [--rvl-i:2]" data-reveal>Every piece is thrown, trimmed and glazed in our Leith workshop, then fired for fourteen hours. When a batch sells out, the next one is three weeks away.</p>
  <ul class="mt-8 grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-[18px] text-left">
    <li class="rounded-[20px] border border-[#eadfd3] bg-white p-[22px] shadow-[0_18px_40px_-30px_rgba(35,29,25,.5)] [--rvl-i:0]" data-reveal><i class="mb-3.5 block size-11 rounded-full bg-[radial-gradient(circle_at_30%_30%,#efe6d8,#c9b79c)]" aria-hidden="true"></i><h3 class="text-[1.05rem] font-bold">Oatmeal speckle mugs</h3><p class="mt-1 text-[.92rem] text-[#5f554d]">350 ml, dishwasher safe, iron flecks in every glaze.</p><b class="mt-3 block text-[#a8481f]">£24 · 28 left</b></li>
    <li class="rounded-[20px] border border-[#eadfd3] bg-white p-[22px] shadow-[0_18px_40px_-30px_rgba(35,29,25,.5)] [--rvl-i:1]" data-reveal><i class="mb-3.5 block size-11 rounded-full bg-[radial-gradient(circle_at_30%_30%,#d6e6dc,#7fa391)]" aria-hidden="true"></i><h3 class="text-[1.05rem] font-bold">Celadon pasta bowls</h3><p class="mt-1 text-[.92rem] text-[#5f554d]">22 cm wide with a low rim that holds a sauce.</p><b class="mt-3 block text-[#a8481f]">£32 · 15 left</b></li>
    <li class="rounded-[20px] border border-[#eadfd3] bg-white p-[22px] shadow-[0_18px_40px_-30px_rgba(35,29,25,.5)] [--rvl-i:2]" data-reveal><i class="mb-3.5 block size-11 rounded-full bg-[radial-gradient(circle_at_30%_30%,#e9b9a1,#9b3f1f)]" aria-hidden="true"></i><h3 class="text-[1.05rem] font-bold">Iron red side plates</h3><p class="mt-1 text-[.92rem] text-[#5f554d]">Glossy on top, raw clay underneath, 19 cm.</p><b class="mt-3 block text-[#a8481f]">£18 · 40 left</b></li>
  </ul>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a5f56] uppercase">Slide in from the sides</span>
<section class="overflow-x-clip rounded-[28px] bg-[#f3ece3] p-[clamp(20px,5vw,56px)]" aria-labelledby="rvl-slide-title">
  <div class="grid items-center gap-[clamp(24px,5vw,56px)] min-[721px]:grid-cols-2">
    <img class="block aspect-[4/3] h-auto w-full rounded-[20px] object-cover" data-reveal="left" src="https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?auto=format&fit=crop&w=900&q=75" alt="Stacks of blue and charcoal glazed plates and bowls on a wooden shelf" width="900" height="675" loading="lazy">
    <div data-reveal="right">
      <p class="text-[.78rem] font-bold tracking-[.14em] text-[#a8481f] uppercase">The dinner set</p>
      <h2 class="mt-2.5 text-[clamp(1.7rem,1.2rem+2vw,2.6rem)] leading-[1.1] font-bold tracking-[-.03em]" id="rvl-slide-title">Four glazes that sit well together</h2>
      <p class="mt-3 max-w-[58ch] text-[#5f554d]">Mix charcoal plates with sky-blue bowls, or buy a single glaze for the whole table. Every set ships in recycled pulp trays.</p>
    </div>
  </div>
  <div class="mt-12 grid items-center gap-[clamp(24px,5vw,56px)] min-[721px]:grid-cols-2">
    <img class="block aspect-[4/3] h-auto w-full rounded-[20px] object-cover min-[721px]:order-2" data-reveal="right" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&w=900&q=75" alt="A tall speckled cup with a raw clay base beside four small white cups" width="900" height="675" loading="lazy">
    <div data-reveal="left">
      <p class="text-[.78rem] font-bold tracking-[.14em] text-[#a8481f] uppercase">Espresso cups</p>
      <h2 class="mt-2.5 text-[clamp(1.7rem,1.2rem+2vw,2.6rem)] leading-[1.1] font-bold tracking-[-.03em]">Small enough for one good shot</h2>
      <p class="mt-3 max-w-[58ch] text-[#5f554d]">Seventy millilitres, a thick wall that holds the heat, and a raw foot that will not slide on the saucer.</p>
    </div>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a5f56] uppercase">Scale in</span>
<ul class="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-6 rounded-[20px] bg-[#7f3416] p-[clamp(28px,5vw,48px)] text-center text-[#fff7ef]" data-reveal="scale" aria-label="Workshop in numbers">
  <li><strong class="block text-[clamp(2rem,1.5rem+2vw,2.9rem)] leading-[1.1] tracking-[-.03em]">1,280 °C</strong><span class="text-[.92rem] text-[#fbe3d3]">Peak kiln temperature</span></li>
  <li><strong class="block text-[clamp(2rem,1.5rem+2vw,2.9rem)] leading-[1.1] tracking-[-.03em]">14 days</strong><span class="text-[.92rem] text-[#fbe3d3]">From wheel to shelf</span></li>
  <li><strong class="block text-[clamp(2rem,1.5rem+2vw,2.9rem)] leading-[1.1] tracking-[-.03em]">6 potters</strong><span class="text-[.92rem] text-[#fbe3d3]">All working in Leith</span></li>
</ul></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a5f56] uppercase">Clip-path wipe</span>
<figure>
  <img class="block aspect-[21/9] h-auto w-full rounded-[20px] object-cover" data-reveal="wipe" src="https://images.unsplash.com/photo-1612196808214-b8e1d6145a8c?auto=format&fit=crop&w=1400&q=75" alt="A round cream vase on a sideboard beside a trailing green plant" width="1400" height="600" loading="lazy">
  <figcaption class="mt-3 text-[.9rem] text-[#5f554d]">The Moon vase, glazed in matt cream. Wipes in from the left as it enters the view.</figcaption>
</figure></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a5f56] uppercase">Inside a scroll container</span>
<div class="h-[420px] overflow-y-auto overflow-x-clip overscroll-contain rounded-[20px] border border-[#eadfd3] bg-[#f3ece3] px-6 pt-6 pb-40 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#a8481f]" data-reveal-root tabindex="0" role="region" aria-label="Kiln diary, scrollable">
  <ol class="grid gap-3">
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-02">2 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Clay delivered</strong>Two tonnes of buff stoneware from Stoke.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-04">4 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Throwing week</strong>Forty mugs and twenty bowls on the wheel.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-09">9 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Trimming and handles</strong>Feet trimmed, handles pulled and attached.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-12">12 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Bisque firing</strong>Ten hours to 1,000 °C, then a slow cool.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-15">15 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Glazing</strong>Oatmeal, celadon and iron red, dipped by hand.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-17">17 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Glaze firing</strong>Fourteen hours to 1,280 °C.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-19">19 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">Kiln opened</strong>Every piece checked, sanded and photographed.</p></li>
    <li class="grid grid-cols-[72px_1fr] gap-3.5 rounded-[14px] border border-[#eadfd3] bg-white px-[18px] py-4" data-reveal><time class="font-bold text-[#a8481f]" datetime="2027-03-20">20 Mar</time><p class="text-[.94rem] text-[#5f554d]"><strong class="block text-[#231d19]">In the shop</strong>The batch goes live at 9 am.</p></li>
  </ol>
</div></div>
CSS
/* Layout and look are utility classes in the markup. The reveal itself is the shared CSS below: elements with
   data-reveal are visible by default and only animate where animation-timeline: view() is supported and reduced
   motion is not requested. The script adds an IntersectionObserver fallback (.rvl-js) for other browsers. */
@keyframes rvl-up{from{filter:opacity(0);transform:translateY(48px)}}
@keyframes rvl-left{from{filter:opacity(0);transform:translateX(-40px)}}
@keyframes rvl-right{from{filter:opacity(0);transform:translateX(40px)}}
@keyframes rvl-scale{from{filter:opacity(0);transform:scale(.82)}}
@keyframes rvl-wipe{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    [data-reveal]{animation:rvl-up linear both;animation-timeline:view();
      animation-range:cover calc(var(--rvl-i,0) * 5%) cover calc(28% + var(--rvl-i,0) * 5%)}
    [data-reveal="left"]{animation-name:rvl-left}
    [data-reveal="right"]{animation-name:rvl-right}
    [data-reveal="scale"]{animation-name:rvl-scale}
    [data-reveal="wipe"]{animation-name:rvl-wipe;animation-range:cover 0% cover 40%}
  }
  @media (prefers-reduced-motion:no-preference) and (max-width:720px){
    [data-reveal="left"],[data-reveal="right"]{animation-name:rvl-up}
  }
}
.rvl-js [data-reveal]{transition:opacity .7s ease,transform .7s cubic-bezier(.2,.7,.2,1),clip-path .9s cubic-bezier(.2,.7,.2,1);
  transition-delay:calc(var(--rvl-i,0) * 90ms)}
.rvl-js [data-reveal]:not(.rvl-in){opacity:0;transform:translateY(48px)}
.rvl-js [data-reveal="left"]:not(.rvl-in){transform:translateX(-40px)}
.rvl-js [data-reveal="right"]:not(.rvl-in){transform:translateX(40px)}
.rvl-js [data-reveal="scale"]:not(.rvl-in){transform:scale(.82)}
.rvl-js [data-reveal="wipe"]:not(.rvl-in){opacity:1;transform:none;clip-path:inset(0 100% 0 0)}
@media (prefers-reduced-motion:reduce){
  [data-reveal],.rvl-js [data-reveal]:not(.rvl-in){animation:none;transition:none;opacity:1;transform:none;clip-path:none}
}
JavaScript
// Fallback for browsers without scroll-driven animations: reveal [data-reveal] elements with IntersectionObserver.
// Each element is observed against its own scroll container ([data-reveal-root]) or the page.
(() => {
  if (CSS.supports('animation-timeline: view()')) return;
  if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const els = document.querySelectorAll('[data-reveal]');
  if (!els.length) return;
  const observers = new Map(), targets = new Map();
  const observerFor = root => {
    if (!observers.has(root)) observers.set(root, new IntersectionObserver((entries, io) => {
      entries.forEach(e => { if (e.isIntersecting) { targets.get(e.target).classList.add('rvl-in'); io.unobserve(e.target); } });
    }, { root, rootMargin: '0px 0px -8% 0px', threshold: 0.12 }));
    return observers.get(root);
  };
  document.documentElement.classList.add('rvl-js');
  els.forEach(el => {
    // a wiped element is fully clipped while hidden, so watch its parent instead
    const target = el.dataset.reveal === 'wipe' ? el.parentElement : el;
    targets.set(target, el);
    observerFor(el.parentElement.closest('[data-reveal-root]')).observe(target);
  });
})();

About this set

Five ways to bring content in as it scrolls into view, shown on a ceramics studio page. Fade up, staggered, lifts a heading, an intro and three product cards one after another. Slide in from the sides brings a photo and its text in from opposite edges on a padded panel, so nothing pokes out of the layout. Scale in grows a stats band from 82 percent. Clip-path wipe uncovers a wide photo from left to right. The last example runs the same fade inside its own scroll container, a kiln diary list. Every element is marked with a data-reveal attribute and is fully visible by default. Where the browser supports scroll-driven animations, an animation with animation-timeline: view() plays as the element crosses the bottom of its scroll container, so the motion follows the scroll position exactly and costs no JavaScript. Browsers without view() get the same effect from a small IntersectionObserver script that hides elements only once it is running and reveals each one when it enters, observing scroll containers separately. Without JavaScript, or with reduced motion requested, nothing is hidden and nothing moves.

What’s included

  • Fade up with stagger, slide from the sides, scale in and clip-path wipe
  • animation-timeline: view() with an IntersectionObserver fallback
  • Works on the page and inside a scroll container
  • Content is visible by default, with no JavaScript or with reduced motion
  • The same data-reveal CSS in the plain, Bootstrap and Tailwind files

Accessibility

Content is never hidden before an animation can run: the reveal only applies inside @supports with prefers-reduced-motion: no-preference, or after the fallback script has started. The scroll container is a labelled region that takes keyboard focus, with a visible focus outline, so arrow keys scroll it. Under reduced motion every element is shown in place with no transition.

Customise it

Change --rvl-distance for the travel, --rvl-i on an element for its stagger step, and the animation-range values for when the reveal starts and ends. Colours are --rvl-ink, --rvl-muted and --rvl-accent; in Tailwind they are the arbitrary colour classes in the markup.