Soft Blur In

Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal.

Preview

Open editor

Usage

Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal. Drop it in as a chapter card, hero opener, or as a beat between scenes — the same headline + subtitle props power the whole text-animation family, so swapping motions takes one field change.

Props

NameTypeDefault
headlinestring"Precision in motion"
subtitlestring"Character by character"

Composition

ID
TextSoftBlurIn
Resolution
1920×1080
FPS
60
Duration
1.7s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import { AbsoluteFill, Easing, interpolate } from "remotion";
import { useDesignFrame } from "../../use-design-frame";
import { useFontReady } from "../../use-font-ready";
import {
  getSubtitleColor,
  resolveTitleStyle,
  snap,
  type TitleProps,
} from "../title-shared";

export type TextSoftBlurInProps = TitleProps;

const APPLE_EASE = Easing.bezier(0.16, 1, 0.3, 1);
const CHAR_EASE = Easing.bezier(0.22, 1, 0.36, 1);

const HEADLINE_START = 8;
const CHAR_DURATION = 54;
const CHAR_STAGGER = 1.5;
const MAX_BLUR_PX = 12;

// Soft-blur-in without inter-character halo bleed.
//
// Per-character filter:blur causes each char's halo to extend into the
// neighbouring char's bounding box. When one char is settled but its
// neighbour is mid-blur, the neighbour's halo edge flickers across the
// settled char's pixels — visible as 1-px back-and-forth jitter even
// though every per-char value is monotonic.
//
// Solution: one filter:blur on the whole <h1>. A single Gaussian kernel
// is applied to the composited result of all chars, so there's no
// per-char halo to bleed. The kernel value is integer-snapped so
// adjacent frames within a step are rasterised identically. Chars
// stagger in via opacity alone — their layout is stable, no transforms.
// The headline-wide blur fades over a window long enough to overlap
// every char's fade-in stagger, giving each char some focus-pull time.
export const TextSoftBlurIn: React.FC<TextSoftBlurInProps> = ({
  headline,
  subtitle,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const s = resolveTitleStyle(clipStyle);
  useFontReady(s.fontFamily);
  const chars = headline.split("");

  const lastCharEnd =
    HEADLINE_START + (chars.length - 1) * CHAR_STAGGER + CHAR_DURATION;

  const headlineProgress = interpolate(
    frame,
    [HEADLINE_START, lastCharEnd],
    [0, 1],
    {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
      easing: APPLE_EASE,
    },
  );
  const headlineBlurPx = Math.round((1 - headlineProgress) * MAX_BLUR_PX);

  const subtitleStart = lastCharEnd + 14;
  const subtitleProgress = interpolate(
    frame,
    [subtitleStart, subtitleStart + 26],
    [0, 1],
    {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
      easing: APPLE_EASE,
    },
  );

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        color: s.color,
        fontFamily: s.fontFamily,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: "0 80px",
        textAlign: "center",
      }}
    >
      <h1
        style={{
          fontSize: 132,
          fontWeight: 700,
          letterSpacing: "-0.045em",
          lineHeight: 1.05,
          margin: 0,
          display: "flex",
          flexWrap: "wrap",
          justifyContent: "center",
          filter: headlineBlurPx > 0 ? `blur(${headlineBlurPx}px)` : undefined,
        }}
      >
        {chars.map((char, i) => {
          const startFrame = HEADLINE_START + i * CHAR_STAGGER;
          const progress = interpolate(
            frame,
            [startFrame, startFrame + CHAR_DURATION],
            [0, 1],
            {
              extrapolateLeft: "clamp",
              extrapolateRight: "clamp",
              easing: CHAR_EASE,
            },
          );
          return (
            <span
              key={i}
              style={{
                display: "inline-block",
                opacity: progress,
                whiteSpace: "pre",
              }}
            >
              {char === " " ? " " : char}
            </span>
          );
        })}
      </h1>

      {subtitle.trim() && (
        <p
          style={{
            fontSize: 38,
            fontWeight: 400,
            letterSpacing: "-0.012em",
            margin: "32px 0 0",
            color: getSubtitleColor(s.color),
            opacity: subtitleProgress,
            transform: `translate3d(0, ${snap((1 - subtitleProgress) * 14)}px, 0)`,
          }}
        >
          {subtitle}
        </p>
      )}
    </AbsoluteFill>
  );
};
Save as TextSoftBlurIn/TextSoftBlurIn.tsx