Toast

A configurable notification toast that slides in from any corner with a spring, holds, then fades out.

Preview

Open editor

Usage

A toast notification you can drop into any scene to mark a moment — render complete, message delivered, deploy successful. Six anchor positions (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right) place it against the edge of the canvas, and the slide direction adjusts automatically.

Pick a variant (info, success, warning, error) to change the icon and accent. durationVisibleSec controls how long the toast holds before it fades back out; tune it to match the surrounding scene length.

Props

NameTypeDefault
titlestring"Render complete"
descriptionstring"Your 30-second video is ready to download."
position"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right""bottom-right"
variant"info" | "success" | "warning" | "error""success"
durationVisibleSecnumber2

Composition

ID
Toast
Resolution
1920×1080
FPS
60
Duration
3.0s

Source

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

import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useVideoConfig,
} from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { snap } from "../../snap";
import { useDesignFrame } from "../../use-design-frame";

export type ToastPosition =
  | "top-left"
  | "top-center"
  | "top-right"
  | "bottom-left"
  | "bottom-center"
  | "bottom-right";

export type ToastVariant = "info" | "success" | "warning" | "error";

export type ToastProps = {
  title: string;
  description: string;
  position: ToastPosition;
  variant: ToastVariant;
  showIcon: boolean;
  durationVisibleSec: number;
  clipStyle?: ClipStyle;
};

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

const VARIANT_ACCENT: Record<ToastVariant, string> = {
  info: "#00bbff",
  success: "#10b981",
  warning: "#f59e0b",
  error: "#ef4444",
};

export const Toast: React.FC<ToastProps> = ({
  title,
  description,
  position,
  variant,
  showIcon,
  durationVisibleSec,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const s = resolveClipStyle(clipStyle, {
    background: "#0f1014",
    color: "#ffffff",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: VARIANT_ACCENT[variant],
  });

  const enterStart = 8;
  const visibleFrames = Math.round(durationVisibleSec * fps);
  const exitStart = Math.min(durationInFrames - 18, enterStart + visibleFrames);

  const enter = spring({
    frame: frame - enterStart,
    fps,
    config: { damping: 16, stiffness: 130, mass: 0.7 },
  });
  const exit = interpolate(frame, [exitStart, exitStart + 18], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });
  const presence = Math.max(0, enter - exit);

  const isTop = position.startsWith("top-");
  const align = position.endsWith("left")
    ? "flex-start"
    : position.endsWith("right")
      ? "flex-end"
      : "center";
  const offset = (1 - presence) * (isTop ? -32 : 32);

  const bgIsDark = isHexDark(s.background);
  const muted = bgIsDark ? "rgba(255,255,255,0.65)" : "rgba(15,16,20,0.62)";
  const border = bgIsDark ? "rgba(255,255,255,0.10)" : "rgba(15,16,20,0.08)";

  return (
    <AbsoluteFill
      style={{
        background: "#ffffff",
        padding: 64,
        fontFamily: s.fontFamily,
        display: "flex",
        flexDirection: "column",
        justifyContent: isTop ? "flex-start" : "flex-end",
        alignItems: align,
      }}
    >
      <div
        style={{
          opacity: presence,
          transform: `translate3d(0, ${snap(offset * 2)}px, 0) scale(${0.96 + presence * 0.04})`,
          minWidth: 760,
          maxWidth: 920,
          background: s.background,
          color: s.color,
          borderRadius: 24,
          padding: "32px 38px",
          display: "flex",
          gap: 24,
          alignItems: "flex-start",
          boxShadow:
            "0 36px 96px rgba(0,0,0,0.45), 0 2px 0 rgba(255,255,255,0.06) inset",
          border: `2px solid ${border}`,
        }}
      >
        {showIcon !== false && (
          <ToastIcon variant={variant} accent={s.accent} />
        )}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div
            style={{
              fontSize: 36,
              fontWeight: 600,
              letterSpacing: "-0.018em",
              marginBottom: description ? 8 : 0,
              lineHeight: 1.15,
            }}
          >
            {title}
          </div>
          {description && (
            <div style={{ fontSize: 26, color: muted, lineHeight: 1.4 }}>
              {description}
            </div>
          )}
        </div>
        <div
          style={{
            fontSize: 26,
            color: muted,
            cursor: "pointer",
            padding: 8,
            margin: -8,
            lineHeight: 1,
          }}
        >
          ✕
        </div>
      </div>
    </AbsoluteFill>
  );
};

function ToastIcon({
  variant,
  accent,
}: {
  variant: ToastVariant;
  accent: string;
}) {
  const path: Record<ToastVariant, string> = {
    success: "M5 13l4 4L19 7",
    info: "M12 8v4m0 4h.01",
    warning:
      "M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z",
    error: "M6 6l12 12M6 18 18 6",
  };
  const wrap: Record<ToastVariant, string> = {
    success: "rgba(16,185,129,0.16)",
    info: `${accent}28`,
    warning: "rgba(245,158,11,0.18)",
    error: "rgba(239,68,68,0.18)",
  };
  const stroke: Record<ToastVariant, string> = {
    success: "#10b981",
    info: accent,
    warning: "#f59e0b",
    error: "#ef4444",
  };
  return (
    <div
      style={{
        flexShrink: 0,
        width: 64,
        height: 64,
        borderRadius: 999,
        background: wrap[variant],
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <svg width="34" height="34" viewBox="0 0 24 24" fill="none">
        <path
          d={path[variant]}
          stroke={stroke[variant]}
          strokeWidth={2.4}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    </div>
  );
}

function isHexDark(color: string): boolean {
  const c = color.trim().toLowerCase();
  if (c.startsWith("#") && c.length === 7) {
    const r = parseInt(c.slice(1, 3), 16);
    const g = parseInt(c.slice(3, 5), 16);
    const b = parseInt(c.slice(5, 7), 16);
    return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.6;
  }
  return c !== "#fff" && c !== "white";
}
Save as Toast/Toast.tsx