DEV Community

Cover image for How to Build a Paper-Cutout Text Animation with Vanilla JavaScript (No Libraries)
Umair Umair
Umair Umair

Posted on

How to Build a Paper-Cutout Text Animation with Vanilla JavaScript (No Libraries)

 If you have ever wanted that "letters cut from paper and dropped onto a page" look for a hero title or intro screen, you don't need GSAP or a heavy animation library. You can build it with plain CSS transforms and about 30 lines of JavaScript.

In this tutorial we'll build a simple paper-cutout drop-in text effect: each letter starts rotated and offset like a scattered paper cutout, then settles into place with a slight bounce.

What we are building

Each character in a string gets wrapped in its own <span>, given a random rotation and offset via CSS custom properties, then animated into its resting position with a staggered delay. The result looks hand-assembled, like stop-motion paper animation.

Step 1: The HTML

<h1 id="paper-text" data-text="Hello Paper World"></h1>
Enter fullscreen mode Exit fullscreen mode

We store the text in a data-text attribute so JavaScript can read it and rebuild it as individual spans.

Step 2: Splitting the text into letters

const el = document.getElementById('paper-text');
const text = el.dataset.text;

el.innerHTML = text
  .split('')
  .map((char, i) => {
    const rotate = (Math.random() * 40 - 20).toFixed(1);
    const x = (Math.random() * 60 - 30).toFixed(1);
    const y = (Math.random() * 60 - 30).toFixed(1);
    return `<span class="paper-char" style="--r:${rotate}deg; --x:${x}px; --y:${y}px; --delay:${i * 0.05}s">${char === ' ' ? '&nbsp;' : char}</span>`;
  })
  .join('');
Enter fullscreen mode Exit fullscreen mode

Each letter gets its own random rotation and offset, stored as CSS custom properties. The delay is based on the letter's position, so the animation reads left to right instead of all letters snapping in at once.

Step 3: The CSS

.paper-char {
  display: inline-block;
  transform: translate(var(--x), var(--y)) rotate(var(--r));
  opacity: 0;
  animation: settle 0.6s cubic-bezier(0.17, 0.89, 0.32, 1.28) forwards;
  animation-delay: var(--delay);
}

@keyframes settle {
  to {
    transform: translate(0, 0) rotate(0deg);
    opacity: 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

The cubic-bezier values give a slight overshoot at the end, so each letter looks like it's settling onto the page rather than just fading in. That overshoot is what sells the "paper" feel.

Step 4: Trigger it on scroll (optional)

If the heading is below the fold, you probably want the animation to play when it enters the viewport, not immediately on page load:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate');
      observer.unobserve(entry.target);
    }
  });
});

observer.observe(el);
Enter fullscreen mode Exit fullscreen mode

Then move the animation property under .paper-char.animate instead of applying it directly, so it only fires once the class is added.

A few tweaks worth trying

  1. Lower the rotation range (20deg to 8deg) for a subtler, more professional effect
  2. Add a faint box-shadow to each span for a real "paper piece" look
  3. Swap translate for larger values if you want a more dramatic scatter-to-order entrance

Why build this from scratch

Doing it this way keeps your bundle size small and gives you full control over timing, easing, and how it reacts to scroll. That said, if you're prototyping quickly, testing a concept for a client, or just don't want to hand-tune keyframes for every project, I built PaperAnimators — a free, browser-based tool that generates this kind of paper-style text and image animation without writing any code. No login, nothing uploaded to a server, everything runs client-side. Worth a look if you want to skip straight to the export.

Wrapping up

That's the whole effect: split the text, randomize each letter's starting transform, animate it back to rest with a bit of overshoot, and optionally gate it behind an IntersectionObserver. It's a good pattern to keep in your toolkit for hero sections, loading screens, or anywhere you want text to feel like it's physically assembling itself.

What other CSS-only text effects would you want a breakdown of? Let me know in the comments.

Top comments (0)