Dirck Mulder
Text Animations||3 min read

Create a Falling Text Animation in React

Characters drop from above with physics-based gravity and bounce, creating a playful and dynamic text reveal effect.

Picture each word as a separate object, falling from its original position and landing with a little bounce. It is chaotic in the best way. Falling Text turns a heading into a mini physics simulation.

The final result

FallingText uses Matter.js to create a real physics simulation where each word becomes a rigid body. Words fall, bounce off walls, pile up on the floor, and can be dragged around with the mouse.

Setting up

This is one of the heavier components. It requires Matter.js for the physics engine.

tsx
import Matter from 'matter-js';

Building the component

The trigger mechanism determines when the physics simulation starts. It supports auto, scroll, click, and hover.

tsx
interface FallingTextProps {
  text?: string;
  highlightWords?: string[];
  trigger?: 'auto' | 'scroll' | 'click' | 'hover';
  backgroundColor?: string;
  wireframes?: boolean;
  gravity?: number;
  mouseConstraintStiffness?: number;
  fontSize?: string;
}

Before the physics starts, the text renders as normal HTML. Each word becomes a <span> element. Highlighted words get special styling.

tsx
const newHTML = words.map(word => {
  const isHighlighted = highlightWords.some(hw => word.startsWith(hw));
  return `<span class="inline-block mx-[2px] select-none ${isHighlighted ? 'text-cyan-500 font-bold' : ''}">
    ${word}
  </span>`;
}).join(' ');
textRef.current.innerHTML = newHTML;

When the simulation starts, we create a Matter.js engine and renderer. The container gets four invisible walls: a floor, ceiling, and two side walls.

tsx
const engine = Engine.create();
engine.world.gravity.y = gravity;

const floor = Bodies.rectangle(width / 2, height + 25, width, 50, { isStatic: true });
const leftWall = Bodies.rectangle(-25, height / 2, 50, height, { isStatic: true });
const rightWall = Bodies.rectangle(width + 25, height / 2, 50, height, { isStatic: true });

For each word span, we read its position in the DOM and create a physics body at the same coordinates. Each body gets a small random initial velocity so words scatter rather than falling straight down.

tsx
const wordBodies = [...wordSpans].map(elem => {
  const rect = elem.getBoundingClientRect();
  const x = rect.left - containerRect.left + rect.width / 2;
  const y = rect.top - containerRect.top + rect.height / 2;

  const body = Bodies.rectangle(x, y, rect.width, rect.height, {
    restitution: 0.8,
    frictionAir: 0.01,
  });
  Matter.Body.setVelocity(body, { x: (Math.random() - 0.5) * 5, y: 0 });
  return { elem, body };
});

Each frame, we sync the DOM element positions to the physics body positions.

tsx
const updateLoop = () => {
  wordBodies.forEach(({ body, elem }) => {
    const { x, y } = body.position;
    elem.style.left = `${x}px`;
    elem.style.top = `${y}px`;
    elem.style.transform = `translate(-50%, -50%) rotate(${body.angle}rad)`;
  });
  requestAnimationFrame(updateLoop);
};

How to use it

tsx
<FallingText
  text="The quick brown fox jumps"
  highlightWords={['fox', 'quick']}
  trigger="click"
  gravity={1}
  mouseConstraintStiffness={0.2}
  fontSize="1.5rem"
/>

Key takeaways

  • Creating physics bodies at the existing DOM positions means the simulation starts from where the text already is, creating a seamless visual transition from static to dynamic.
  • Setting restitution: 0.8 gives words a satisfying bounce without making them fly back up indefinitely.
  • The mouse constraint with a low stiffness lets users push words around without them immediately snapping to the pointer, which feels more physical.