UI/Components/Card elements/Toast Card

Toast Card

A Remotion video component that animates a toast notification with customizable entry, visible, and exit phases, and flexible positioning.

Notification?

Could be used as an interview notification.

0:00 / 0:07

Installation

CLI

npx shadcn@latest add "https://clippkit.com/r/toast-card"

Manual

Create a new file, for example, at src/components/clippkit/toast-card.tsx (or your preferred location) and paste the following code into it.

"use client";
 
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
 
export type PositionPreset =
  | "bottom-left"
  | "bottom-right"
  | "top-left"
  | "top-right"
  | "center";
 
interface ToastCardProps {
  title?: string;
  message?: string;
  titleColor?: string;
  messageColor?: string;
  backgroundColor?: string;
  titleFontSize?: string;
  messageFontSize?: string;
  width?: string;
  padding?: string;
  borderRadius?: string;
  borderColor?: string;
  borderWidth?: string;
  borderStyle?: "solid" | "dashed" | "dotted";
  boxShadow?: string;
  positionPreset?: PositionPreset;
  margin?: string; // e.g., "20px"
 
  entryDurationInFrames?: number;
  visibleDurationInFrames?: number;
  exitDurationInFrames?: number;
 
  damping?: number;
  mass?: number;
  stiffness?: number;
 
  fontFamily?: string;
  slideOffset?: number; // e.g., 50 (pixels) for vertical slide
}
 
export function ToastCard({
  title = "Success!",
  message = "Your action was completed.",
  titleColor = "var(--card-foreground)",
  messageColor = "var(--card-foreground)",
  backgroundColor = "var(--card)",
  titleFontSize = "1.1rem",
  messageFontSize = "0.9rem",
  width = "300px",
  padding = "15px 20px",
  borderRadius = "10px",
  borderColor = "var(--border)",
  borderWidth = "1px",
  borderStyle = "solid",
  boxShadow = "0 4px 12px rgba(0,0,0,0.1)",
  positionPreset = "bottom-left",
  margin = "20px",
 
  entryDurationInFrames = 30,
  visibleDurationInFrames = 120,
  exitDurationInFrames = 30,
 
  damping = 25,
  mass = 0.7,
  stiffness = 180,
  fontFamily = "Inter, sans-serif",
  slideOffset = 50, // pixels to slide in/out by
}: ToastCardProps) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  const exitAnimationStartFrame =
    entryDurationInFrames + visibleDurationInFrames;
  const totalComponentAnimationDuration =
    entryDurationInFrames + visibleDurationInFrames + exitDurationInFrames;
 
  const entryAnimProgress = spring({
    frame,
    fps,
    from: 0,
    to: 1,
    durationInFrames: entryDurationInFrames,
    config: { damping, mass, stiffness },
  });
 
  const exitAnimProgress = spring({
    frame: frame - exitAnimationStartFrame,
    fps,
    from: 0,
    to: 1,
    durationInFrames: exitDurationInFrames,
    config: { damping, mass, stiffness: stiffness / 1.5 }, // Softer exit
  });
 
  const opacity =
    interpolate(entryAnimProgress, [0, 1], [0, 1]) *
    interpolate(exitAnimProgress, [0, 1], [1, 0]);
 
  let yTranslateStart = 0;
  const scaleStart = 0.95;
  const scaleEnd = 1;
 
  if (positionPreset === "bottom-left" || positionPreset === "bottom-right") {
    yTranslateStart = slideOffset; // Start from below
  } else if (positionPreset === "top-left" || positionPreset === "top-right") {
    yTranslateStart = -slideOffset; // Start from above
  } else if (positionPreset === "center") {
    yTranslateStart = slideOffset; // Center also slides, use full slideOffset from bottom
  }
 
  const yPos =
    interpolate(entryAnimProgress, [0, 1], [yTranslateStart, 0]) +
    interpolate(exitAnimProgress, [0, 1], [0, yTranslateStart]); // Exit to the same direction it came from
 
  const scale =
    positionPreset === "center"
      ? interpolate(entryAnimProgress, [0, 1], [scaleStart, scaleEnd]) *
        interpolate(exitAnimProgress, [0, 1], [scaleEnd, scaleStart])
      : 1;
 
  const transformProperties: string[] = [];
  if (positionPreset === "center") {
    transformProperties.push(`translate(-50%, -50%)`); // Center alignment first
  }
  transformProperties.push(`translateY(${yPos}px)`);
  if (scale !== 1) {
    // Only add scale if it's not 1 to keep transform shorter
    transformProperties.push(`scale(${scale})`);
  }
 
  const cardStyle: React.CSSProperties = {
    position: "absolute",
    width,
    padding,
    background: backgroundColor,
    borderRadius,
    borderColor,
    borderWidth,
    borderStyle,
    boxShadow,
    fontFamily,
    display: "flex",
    flexDirection: "column",
    gap: "5px",
    boxSizing: "border-box",
    opacity,
    transform: transformProperties.join(" "),
  };
 
  if (positionPreset === "center") {
    cardStyle.top = "50%";
    cardStyle.left = "50%";
  } else {
    // Vertical positioning
    if (positionPreset.includes("bottom")) {
      cardStyle.bottom = margin;
    } else if (positionPreset.includes("top")) {
      cardStyle.top = margin;
    }
 
    // Horizontal positioning
    if (positionPreset.includes("left")) {
      cardStyle.left = margin;
      cardStyle.right = "auto"; // Explicitly set right to auto
    } else if (positionPreset.includes("right")) {
      cardStyle.right = margin;
      cardStyle.left = "auto"; // Explicitly set left to auto
    } else {
      // Fallback or default horizontal positioning if needed
      // For current defined presets, this path shouldn't be taken.
      // If it were, centering horizontally might be a safe default:
      // cardStyle.left = "50%";
      // if (!transformProperties.some(t => t.startsWith("translateX")) && !positionPreset.includes("center")) {
      // transformProperties.unshift("translateX(-50%)");
      // }
    }
  }
 
  // Don't render if fully exited and transparent (past its animation lifecycle)
  if (frame >= totalComponentAnimationDuration && opacity < 0.01) {
    return null;
  }
 
  return (
    <div style={cardStyle}>
      {title && (
        <h3
          style={{
            margin: 0,
            fontSize: titleFontSize,
            fontWeight: "bold",
            color: titleColor,
          }}
        >
          {title}
        </h3>
      )}
      {message && (
        <p
          style={{
            margin: 0,
            fontSize: messageFontSize,
            color: messageColor,
            opacity: 0.9,
          }}
        >
          {message}
        </p>
      )}
    </div>
  );
}
Update the import paths in your Remotion compositions if you placed the file in a different location than shown in the usage examples.

Usage

Once the ToastCard component is added to your project (either via CLI or Manually), you can integrate it into your Remotion project by importing it and using it within a Composition or directly with the @remotion/player.

Prerequisite

Ensure you have a Remotion project set up. If not, please refer to the Remotion documentation to get started.

Project Structure Example

Here’s an example folder layout showing where to place the component and how it fits into a typical Remotion project

app/main.tsx
import { Player } from "@remotion/player";
 
import ToastCard, { PositionPreset } from "../components/toast-card"; // Adjust path as necessary
 
export function ToastCardDemo() {
  const toastCardProps = {
    title: "Notification?",
    message: "Could be used as an interview notification.",
    titleColor: "var(--card-foreground)",
    messageColor: "var(--card-foreground)",
    backgroundColor: "var(--card)",
    titleFontSize: "1.2rem",
    messageFontSize: "0.8rem",
    width: "320px",
    padding: "18px 22px",
    borderRadius: "8px",
    borderColor: "var(--ring)",
    borderWidth: "1px",
    borderStyle: "solid" as const,
    boxShadow: "0 6px 15px rgba(0,0,0,0.2)",
    positionPreset: "bottom-right" as PositionPreset,
    margin: "40px",
    entryDurationInFrames: 25,
    visibleDurationInFrames: 150,
    exitDurationInFrames: 25,
    damping: 18,
    mass: 0.9,
    stiffness: 110,
    slideOffset: 60,
  };
 
  return (
    <Player
      component={ToastCard}
      inputProps={toastCardProps}
      durationInFrames={
        toastCardProps.entryDurationInFrames +
        toastCardProps.visibleDurationInFrames +
        toastCardProps.exitDurationInFrames +
        30
      } // Add buffer to see full exit
      compositionWidth={500}
      compositionHeight={400}
      fps={30}
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "var(--background)", // Or a contrasting color
      }}
      autoPlay
      controls
      loop
    />
  );
}

Define a Composition

In your Remotion project's entry file (commonly src/Root.tsx, src/index.tsx, app/main.tsx), import ToastCard and define a Composition.

app/main.tsx (or equivalent)
import { Player } from "@remotion/player";

import ToastCard from "@/components/clippkit/toast-card"; // Assuming you placed it in src/components/clippkit

export default function MyToastPlayer() {
  const toastProps = {
    title: "Update Available",
    message: "A new version of the software is ready to install.",
    backgroundColor: "oklch(0.7 0.15 250)", // A nice blue
    titleColor: "#FFFFFF",
    messageColor: "#F0F0F0",
    positionPreset: "top-right" as const,
    entryDurationInFrames: 30,
    visibleDurationInFrames: 180, // Show for 6 seconds at 30 FPS
    exitDurationInFrames: 30,
    width: "350px",
  };

  const totalDuration =
    toastProps.entryDurationInFrames +
    toastProps.visibleDurationInFrames +
    toastProps.exitDurationInFrames +
    60; // Extra 60 frames buffer

  return (
    <Player
      component={ToastCard}
      inputProps={toastProps}
      durationInFrames={totalDuration}
      compositionWidth={1280} // Typical video width
      compositionHeight={720} // Typical video height
      fps={30}
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "var(--background-alt)", // A slightly different background
      }}
      autoPlay
      controls
      loop
    />
  );
}

API Reference

The component exported as ToastCard (e.g., from apps/docs/registry/default/ui/toast-card.tsx or your project's component path) accepts the following props to customize its animation and appearance:

PropTypeDefault ValueDescription
titlestring"Success!"The main title text for the toast.
messagestring"Your action was completed."The secondary message text for the toast.
titleColorstring"var(--card-foreground)"CSS color for the title text.
messageColorstring"var(--card-foreground)"CSS color for the message text.
backgroundColorstring"var(--card)"CSS background for the toast card.
titleFontSizestring"1.1rem"CSS font size for the title.
messageFontSizestring"0.9rem"CSS font size for the message.
widthstring"300px"CSS width of the toast card.
paddingstring"15px 20px"CSS padding inside the toast card.
borderRadiusstring"10px"CSS border radius of the toast card.
borderColorstring"var(--border)"CSS border color of the toast card.
borderWidthstring"1px"CSS border width of the toast card.
borderStyle"solid" | "dashed" | "dotted""solid"CSS border style of the toast card.
boxShadowstring"0 4px 12px rgba(0,0,0,0.1)"CSS box shadow for the toast card.
positionPresetPositionPreset"bottom-left"Predefined position: "bottom-left", "bottom-right", "top-left", "top-right", "center".
marginstring"20px"Outer margin for the toast when not centered (e.g., distance from screen edges).
entryDurationInFramesnumber30Duration of the entry animation (slide-in, fade-in) in frames.
visibleDurationInFramesnumber120Duration the toast remains fully visible on screen in frames.
exitDurationInFramesnumber30Duration of the exit animation (slide-out, fade-out) in frames.
dampingnumber15Damping for the spring animations. Controls how quickly oscillations die down.
massnumber0.8Mass for the spring animations.
stiffnessnumber120Stiffness for the spring animations.
fontFamilystring"Inter, sans-serif"CSS font family for the text in the toast.
slideOffsetnumber50The distance (in pixels) the toast slides during entry/exit animations (for non-center positions).

Type Definition for PositionPreset

type PositionPreset =
  | "bottom-left"
  | "bottom-right"
  | "top-left"
  | "top-right"
  | "center";

On this page