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.
import Matter from 'matter-js';Building the component
The trigger mechanism determines when the physics simulation starts. It supports auto, scroll, click, and hover.
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.
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.
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.
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.
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
<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.8gives 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.