Skip to Content
AnimationsPulsating Button

Pulsating Button

A button is pressed briefly and generates a series of concentric pulses radiating outwards in the shape of the button’s outline.

Requirements

Schema

Root.tsx
const schema = z.object({ /** The radius of the button's border */ borderRadius: z.number(), /** The background color of the button */ backgroundColor: z.string(), /** The foreground color of the button */ foregroundColor: z.string(), /** The horizontal padding of the button */ paddingHorizontal: z.number(), /** The vertical padding of the button */ paddingVertical: z.number(), /** The font size of the button */ fontSize: z.number(), /** The text of the button */ text: z.string(), /** The icon of the button */ icon: iconNameSchema, /** The color of the icon's stroke */ strokeColor: z.string().optional(), /** If true, the icon and text will be swapped. */ swapIconAndText: z.boolean(), pulse: z.object({ /** Percentage of overlap between pulses */ overlap: z.number(), /** Duration of each pulse */ duration: z.number(), /** Number of pulses */ numberOfPulses: z.number(), }), click: z.object({ /** The frame at which the click animation starts */ start: z.number(), /** The frame at which the click animation ends */ end: z.number(), }), });

Implementation

The button is centered on the screen

<Remotion.AbsoluteFill style={{ backgroundColor: "white", display: "flex", alignItems: "center", justifyContent: "center", }} > {/* Button */} </Remotion.AbsoluteFill>

The text and icon positions should be swappable

Props


Normal

We can control the text and icon positions by passing a swapIconAndText prop to the PulsatingButton component, which holds a boolean value.

Root.tsx
<Composition id="PulsatingButton" component={PulsatingButton} defaultProps={{ swapIconAndText: true, }} />

Then, in the button component, we can conditionally set flexDirection based on the swapIconAndText prop.

PulsatingButton.tsx
<div style={{ display: "flex", alignItems: "center", flexDirection: swapIconAndText ? "row-reverse" : "row", }} > {/* Icon */} {iconSvg} {/* Text */} <span>{text}</span> </div>

Upon click, the button rapidly scales down and then returns to its original size, simulating the effect of a physical press

To simulate a button press, we need to identify when click event starts (click.start) and when it ends (click.end), both in terms of frames.

Using these two parameters, we are able to smoothly interpolate the button’s scale based on the current frame of the animation.

We also apply a clamp extrapolation on both ends to ensure that the scale remains constrained between 0 and 1, preventing any unintended scaling effects.

PulsatingButton.tsx
const frame = Remotion.useCurrentFrame(); const pressScale = Remotion.interpolate( frame, // Interpolated value [ click.startAt, // Starting point click.startAt + click.duration / 2, // Middle point click.startAt + click.duration, // Ending point ], [1, 0.9, 1], // Values to interpolate between { extrapolateRight: "clamp", extrapolateLeft: "clamp" }, // Restrict to the range defined );

The pulse effect is triggered when the release animation is complete

The click.end parameter tells us when the click event ends, which is precisely the moment the pulse effect should start.

We use Remotion.Sequence component to trigger the pulse effect from click.end onwards.

PulsatingButton.tsx
<Remotion.Sequence from={click.end}> <Pulse /> </Remotion.Sequence>

The pulse effect consists of a series of concentric, expanding pulses that mirror the button’s outline shape

How to create the outline

To achieve the desired effect, we need to ensure that the pulse matches the exact shape of the button.

To do that, we create a button “double”, with some slight changes:

  • Text and icon are rendered, but are not visible.
  • The background color is set to transparent.
PulsatingButton.tsx
<div style={{ backgroundColor: "transparent", }} > {/* Icon */} <Icon name={icon} size={60} color="transparent" /> {/* Text */} <span style={{ fontSize, opacity: 0, }} > {text} </span> </div>

The only visible element should be the outline, which is necessary for the pulse effect.

PulsatingButton.tsx
<div style={{ backgroundColor: "transparent", border: `2px solid ${strokeColor}`, }} > {/* Icon */} <Icon name={icon} size={60} color="transparent" /> {/* Text */} <span style={{ fontSize, opacity: 0, }} > {text} </span> </div>
How to make the outline pulse

First we need to increase the scale of the outline to create an expanding animation. We do this by interpolating the current frame, for a arbitrary duration (pulse.duration), with a scale range of 1 to 3.5 — enough to expand the outline beyond the viewport’s boundaries.

By applying clamp extrapolation, we ensure that the scale never exceeds the defined range, preventing any unintended scaling effects.

const scale = Remotion.interpolate(frame, [0, pulse.duration], [1, 3.5], { extrapolateRight: "clamp", extrapolateLeft: "clamp", }); return ( <div style={{ transform: `scale(${scale})`, }} > {/* ... */} </div> );
Creating multiple pulses in a row

Now that we have a pulse effect, the next step is to replicate it multiple times, with a slight delay between each pulse, as if they’re rippling outwards.

To do this, we can use the Remotion.Sequence component again, but this time we need to pass a from prop that is the sum of the previous pulse’s duration and overlap.

{ pulses.map((_, index) => ( <Remotion.Sequence key={index} // `Math.floor()` ensures the start time is an integer, avoiding fractional frames from={click.end + Math.floor(index * pulse.duration * pulse.overlap)} // Each pulse lasts for `pulse.duration` frames durationInFrames={pulse.duration} > <Pulse {...props} key={index} strokeColor={colorVariations[index]} /> </Remotion.Sequence> )); }
  • pulse.duration tells us how long each pulse lasts.
  • pulse.overlap shows how much of the next pulse (as a percentage) starts before the current one ends. This makes the pulses flow smoothly into each other.
Last updated on