Skip to content
animorio
IT
Back to the library

The paint roller transition

Transitions, SVGGSAP, CSS onlyAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

A rich, colourful page change for a painters and decorators’ site: on a click masking tape runs along the edges, the roller loads up with paint in the tray and paints the new page in lanes, down and up, with ridges at the edges of each pass, specks and drips; then it flies off, the fresh paint shines and dries, the tape peels away and the new headline arrives.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The painter’s roller transition</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <!-- the first page: a painters and decorators studio, on warm cream walls -->
    <section class="page page--a is-on" data-page="a" aria-label="Rooms">
      <header class="bar">
        <span class="brand"><svg class="brand__mark" viewBox="0 0 20 20" aria-hidden="true"><rect x="2" y="3" width="16" height="7" rx="2"/><path d="M18 6.5h1.2v5H10v3"/><rect x="8.6" y="14" width="2.8" height="5" rx="1"/></svg>Fresh Coat</span>
        <nav class="nav" aria-label="Pages">
          <a class="nav__link is-here" href="#rooms" aria-current="page">Rooms</a>
          <a class="nav__link" href="#colours" data-go="b">Colours</a>
        </nav>
      </header>
      <div class="body">
        <p class="kicker" data-late>Painters &amp; decorators</p>
        <h1 class="title" data-title><span class="title__line"><span class="title__in">Rooms, painted</span></span><span class="title__line"><span class="title__in">by hand.</span></span></h1>
        <p class="lede" data-late>Two coats, crisp edges, no drips left behind. We tape, we roll, we tidy up.</p>
        <a class="go" href="#colours" data-go="b" data-autoclic data-late>
          <span class="go__chip" aria-hidden="true"></span>
          <span class="go__text">See this season’s colour</span>
          <svg class="go__arrow" viewBox="0 0 26 12" aria-hidden="true"><path d="M1 6h22"/><path d="M18.5 1.5 23.5 6l-5 4.5"/></svg>
        </a>
      </div>
      <aside class="chips" data-late aria-hidden="true">
        <span class="chip" style="--c: #efe3cc"><b>Oat milk</b></span>
        <span class="chip" style="--c: #d9b98f"><b>Biscuit</b></span>
        <span class="chip" style="--c: #c9734f"><b>Clay pot</b></span>
      </aside>
      <footer class="foot" data-late><span>London &amp; around</span><span>Free colour visits</span></footer>
    </section>

    <!-- the second page: the colour of the season, a deep lagoon green -->
    <section class="page page--b" data-page="b" aria-label="Colours" aria-hidden="true">
      <header class="bar">
        <span class="brand"><svg class="brand__mark" viewBox="0 0 20 20" aria-hidden="true"><rect x="2" y="3" width="16" height="7" rx="2"/><path d="M18 6.5h1.2v5H10v3"/><rect x="8.6" y="14" width="2.8" height="5" rx="1"/></svg>Fresh Coat</span>
        <nav class="nav" aria-label="Pages">
          <a class="nav__link" href="#rooms" data-go="a">Rooms</a>
          <a class="nav__link is-here" href="#colours" aria-current="page">Colours</a>
        </nav>
      </header>
      <div class="body">
        <p class="kicker" data-late>Colour of the season</p>
        <h1 class="title" data-title><span class="title__line"><span class="title__in">Lagoon,</span></span><span class="title__line"><span class="title__in">deep and calm.</span></span></h1>
        <p class="lede" data-late>A green with a drop of blue, matt on walls, satin on the woodwork.</p>
        <a class="go" href="#rooms" data-go="a" data-autoclic data-late>
          <span class="go__chip" aria-hidden="true"></span>
          <span class="go__text">Back to the rooms</span>
          <svg class="go__arrow" viewBox="0 0 26 12" aria-hidden="true"><path d="M1 6h22"/><path d="M18.5 1.5 23.5 6l-5 4.5"/></svg>
        </a>
      </div>
      <aside class="chips" data-late aria-hidden="true">
        <span class="chip" style="--c: #2c8a83"><b>Shallows</b></span>
        <span class="chip" style="--c: #1f6f6a"><b>Lagoon</b></span>
        <span class="chip" style="--c: #134a47"><b>Deep end</b></span>
      </aside>
      <footer class="foot" data-late><span>London &amp; around</span><span>Free colour visits</span></footer>
    </section>

    <!-- the painting: masking tape, the tray, the roller, ridges, drips and specks -->
    <svg class="paint" data-paint aria-hidden="true">
      <defs>
        <clipPath id="paintClip" clipPathUnits="userSpaceOnUse" data-clip></clipPath>
        <pattern id="nap" width="5" height="7" patternUnits="userSpaceOnUse" data-nap>
          <path d="M1 0v3.4M3.5 2.2v3.6M2.2 4.6v2.4" stroke="rgba(0,0,0,.28)" stroke-width=".9" stroke-linecap="round"/>
          <path d="M4.4 .4v1.8M.4 4v1.6" stroke="rgba(255,255,255,.22)" stroke-width=".8" stroke-linecap="round"/>
        </pattern>
        <pattern id="streak" width="11" height="160" patternUnits="userSpaceOnUse" data-streak>
          <path d="M2.5 0v70M6 88v60M8.5 22v34" stroke="rgba(0,0,0,.1)" stroke-width="1.4"/>
          <path d="M1 50v44M4 110v40M9.5 0v18" stroke="rgba(255,255,255,.12)" stroke-width="1.2"/>
        </pattern>
        <linearGradient id="sleeveShade" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="#fff" stop-opacity=".34"/>
          <stop offset=".28" stop-color="#fff" stop-opacity=".06"/>
          <stop offset=".7" stop-color="#000" stop-opacity=".08"/>
          <stop offset="1" stop-color="#000" stop-opacity=".34"/>
        </linearGradient>
        <linearGradient id="metal" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stop-color="#e8ecef"/><stop offset=".45" stop-color="#9aa3aa"/><stop offset="1" stop-color="#6c757c"/>
        </linearGradient>
        <linearGradient id="wood" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stop-color="#a86b37"/><stop offset=".45" stop-color="#d49a5e"/><stop offset="1" stop-color="#9a5f2e"/>
        </linearGradient>
        <linearGradient id="trayBody" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stop-color="#f4f1ec"/><stop offset="1" stop-color="#cfc8be"/>
        </linearGradient>
        <linearGradient id="sheen" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0" stop-color="#fff" stop-opacity="0"/>
          <stop offset=".5" stop-color="#fff" stop-opacity=".42"/>
          <stop offset="1" stop-color="#fff" stop-opacity="0"/>
        </linearGradient>
      </defs>

      <!-- on the fresh paint, clipped to what is painted -->
      <g clip-path="url(#paintClip)">
        <rect class="streaks" data-streaks fill="url(#streak)"/>
        <g data-ridges></g>
        <g data-drips></g>
        <rect class="sheen" data-sheen fill="url(#sheen)"/>
      </g>
      <g data-specks></g>

      <!-- the tray, with its ribbed ramp and a pool of paint -->
      <g class="tray" data-tray>
        <path class="tray__shadow" data-tray-shadow/>
        <path class="tray__body" data-tray-body fill="url(#trayBody)"/>
        <path class="tray__pool" data-tray-pool/>
        <path class="tray__poolshine" data-tray-shine/>
        <g class="tray__ribs" data-tray-ribs></g>
        <path class="tray__lip" data-tray-lip/>
      </g>

      <!-- the roller: wooden handle, wire frame, a fluffy sleeve full of paint -->
      <g class="roller" data-roller>
        <path class="roller__shadow" data-r-shadow/>
        <path class="roller__wire" data-r-wire/>
        <path class="roller__wireshine" data-r-wireshine/>
        <rect class="roller__ferrule" data-r-ferrule fill="url(#metal)"/>
        <path class="roller__handle" data-r-handle fill="url(#wood)"/>
        <g class="roller__grain" data-r-grain></g>
        <circle class="roller__hole" data-r-hole/>
        <g class="roller__sleeve" data-r-sleeve>
          <rect class="sleeve__base" data-s-base/>
          <rect class="sleeve__paint" data-s-paint/>
          <rect class="sleeve__nap" data-s-nap fill="url(#nap)"/>
          <rect class="sleeve__shade" data-s-shade fill="url(#sleeveShade)"/>
          <ellipse class="sleeve__cap" data-s-cap-l/>
          <ellipse class="sleeve__cap" data-s-cap-r/>
        </g>
      </g>
    </svg>

    <!-- masking tape along the top and the bottom edge -->
    <div class="tape tape--top" data-tape aria-hidden="true"><span class="tape__tear"></span></div>
    <div class="tape tape--bottom" data-tape aria-hidden="true"><span class="tape__tear"></span></div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

49 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The paint roller transition” from Animorio and fit it into my site

What it does

Fresh Coat, a made-up painters and decorators firm, has two pages: the rooms, on cream walls, and the colour of the season, a deep lagoon green. On a click on ‘See this season’s colour’ two strips of masking tape run along the top and bottom edges, and from below a plastic tray rises with its ribbed ramp and a pool of green. The roller arrives, still white and dry, with its wooden handle, ferrule and steel frame: it settles into the pool, squashing a little, rolls up and down the ramp twice, and meanwhile the nap fills with colour and turns. Then it flies to the top left and paints: down the first lane, up the second, down the third, each as wide as the roller. Every press flicks a few drops onto the wall around; behind the roller the paint leaves the lines of the nap and two darker ridges at the edges of the pass, and from the finished lanes two drips slowly run down with a drop at the end. After the last lane the roller flies off turning, a light sweeps over the fresh paint and the ridges, lines and drips fade as it dries; the tape peels from its end and the words of the new page rise from under their line, gaining weight. Another click takes you back to the rooms, painted cream.

How it works

The two pages and the clip

The two pages sit one over the other. On a click the new page becomes visible above the old one but clipped by an SVG clipPath (clip-path: url(#paintClip) on the HTML element) holding one rectangle per lane, zero high: what the roller paints is the new page being uncovered, with its background colour. The new page’s words stay hidden until the paint is dry.

The roller

The roller is an SVG group with the centre of the sleeve at 0,0: GSAP moves it with x and y and turns it around that point (svgOrigin 0 0). The sleeve has four layers: the white base, the paint going from clear to full as it loads, the nap (a pattern of short strokes) and a light-and-shade gradient. As the roller moves, the nap’s patternTransform slides by the distance travelled: the sleeve seems to turn.

The lanes

There are as many lanes as fit (four on a computer, three on a phone), as wide as the roller. For each one an object goes from 0 to 1 in LANE_TIME seconds with power1.inOut: on every frame the roller moves down (or up, in the odd lanes) and the lane’s clip rectangle stretches to just past the roller. Every press starts with a 0.08 second squash there and back and a few specks flying off the contact point.

Ridges, lines and drips

Over the new page, inside the same clip, there are the nap lines (a pattern of vertical strokes), two ridges per lane (a darker and a lighter stroke that wobble slightly) and the drips: a rounded rectangle stretching with scaleY from its top end and a drop moving down with it. So they only appear where the paint has already been.

Drying and the tape

Once the roller is out, a gradient band of light crosses the page and ridges, lines, drips and specks fade in 0.9 seconds. The two tape strips (divs with a striped gradient and a torn edge made with clip-path) peel away from their right end, then the headline lines rise from under their mask going from weight 300 to 560.

Reduced motion

With prefers-reduced-motion there is no tape, tray or roller: the new page fades in over 0.6 seconds, already set.

Values to change

NameFileDefaultWhat it changes
LANESscript.js4The lanes of the pass on a computer: the roller is as wide as one lane.
LANES_PHONEscript.js3The lanes on a phone.
LANE_TIMEscript.js0.62How many seconds the roller takes for one lane.
DIPSscript.js2How many times the roller goes up and down the tray’s ramp.
DRIPSscript.js2The drips for each lane.
SPECKSscript.js7The specks at each press of the roller.
SHEENscript.js0.42How much the fresh paint shines when the light passes (0 none).
TAPEscript.jstrueThe masking tape on the edges.
WORD_TIMEscript.js0.8How many seconds a headline line takes to arrive.
--creamstyle.css#f3ead8The cream of the first page’s walls (and of the paint on the way back).
--lagoonstyle.css#1f6f6aThe lagoon green of the second page and of the paint.
--accentstyle.css#e2582bThe terracotta of the button.
--tapestyle.css#e9d8a6The colour of the masking tape.

Try them in the Playground

When to use it

For the page change of a painters and decorators’ site, a paint shop, an interior design or renovation studio, or to move between two colours of a collection.

On phones

The same animations with three lanes instead of four, a smaller roller and tray, a smaller headline and colour chips. A tap changes page.

With reduced motion

No tape, tray or roller: the new page fades in, already set.

Pitfalls

  • Chrome does not apply an SVG mask taken with mask: url(#id) to an HTML element, but it does apply a clipPath: to uncover the page lane by lane you need clip-path: url(#paintClip), with clipPathUnits userSpaceOnUse and the SVG viewBox equal to the stage size in pixels.
  • The nap pattern is in userSpaceOnUse inside the roller group, so it travels with the roller: to make it turn, moving the roller is not enough, its patternTransform has to slide by the distance travelled.

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.

Testing

Tested on 23 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All animations