UI/Components/Loaders/Circular Loader

Circular Loader

A customizable circular loader component for Remotion.

A simple, animated circular loading indicator with a text label in the center.

0%
0:00 / 0:04

Installation

CLI

npx shadcn@latest add "https://clippkit.com/r/circular-loader"

Manual

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

"use client";
 
import React from "react";
import { Easing, interpolate, useCurrentFrame } from "remotion";
 
export interface CircularProgressProps {
  // Overall appearance & behavior
  size?: number; // Overall size of the component
  progressSource?: "time" | "prop"; // 'time' for animation, 'prop' for external control
  progress?: number; // Current progress (0-100), used if progressSource is 'prop'
  durationInFrames?: number; // Duration for one full 0-100% cycle if progressSource is 'time'
  loopProgress?: boolean; // Whether the progress animation should loop
 
  // Background (Track) Circle
  showTrack?: boolean;
  trackColor?: string;
  trackStrokeWidth?: number;
 
  // Progress Arc
  progressStrokeWidth?: number;
  progressStrokeLinecap?: "butt" | "round" | "square";
  progressColorMode?: "solid" | "gradient";
  progressSolidColor?: string;
  progressGradientStartColor?: string;
  progressGradientEndColor?: string;
 
  // Rotating Dots
  showRotatingDots?: boolean;
  dotColor?: string;
  dotRadius?: number;
  dotDistanceFromCenter?: number; // Distance of the dot from the center of the main circle
  rotationSpeed?: number; // Multiplier for rotation speed, positive for clockwise, negative for counter-clockwise
 
  // Pulse Effect
  enablePulse?: boolean;
  pulseMagnitude?: number; // e.g., 0.05 for 5% pulse
  pulseSpeed?: number; // Speed of the pulse animation
 
  // Percentage Text
  showPercentageText?: boolean;
  textColor?: string;
  textSize?: string | number;
  textFontWeight?: string | number;
  textStyle?: React.CSSProperties;
 
  // Container
  containerStyle?: React.CSSProperties;
}
 
export function CircularLoader({
  // Overall appearance & behavior
  size = 200,
  progressSource = "time",
  progress: propProgress = 0,
  durationInFrames = 90,
  loopProgress = true,
 
  // Background (Track) Circle
  showTrack = true,
  trackColor = "rgba(255, 255, 255, 0.1)",
  trackStrokeWidth = 12,
 
  // Progress Arc
  progressStrokeWidth = 12,
  progressStrokeLinecap = "round",
  progressColorMode = "gradient",
  progressSolidColor = "#3b82f6",
  progressGradientStartColor = "#3b82f6",
  progressGradientEndColor = "#1e3a8a",
 
  // Pulse Effect
  enablePulse = true,
  pulseMagnitude = 0.05,
  pulseSpeed = 10,
 
  // Percentage Text
  showPercentageText = true,
  textColor = "white",
  textSize = "3rem",
  textFontWeight = "bold",
  textStyle,
 
  // Container
  containerStyle,
}: CircularProgressProps) {
  const frame = useCurrentFrame();
  // const { fps } = useVideoConfig(); // fps is not used in this version
 
  const actualRadius =
    (size - Math.max(trackStrokeWidth, progressStrokeWidth)) / 2;
  const circumference = 2 * Math.PI * actualRadius;
 
  const currentProgress = React.useMemo(() => {
    if (progressSource === "prop") {
      return Math.min(100, Math.max(0, propProgress));
    }
    // Time-based progress
    const totalFramesForCycle = durationInFrames;
    const currentFrameInCycle = loopProgress
      ? frame % totalFramesForCycle
      : Math.min(frame, totalFramesForCycle);
    return interpolate(
      currentFrameInCycle,
      [0, totalFramesForCycle],
      [0, 100],
      { extrapolateRight: "clamp", easing: Easing.linear }
    );
  }, [frame, progressSource, propProgress, durationInFrames, loopProgress]);
 
  const strokeDashoffset =
    circumference - (currentProgress / 100) * circumference;
  const pulse = enablePulse
    ? 1 + Math.sin(frame / pulseSpeed) * pulseMagnitude
    : 1;
 
  const mainContainerStyle: React.CSSProperties = {
    position: "relative",
    width: size,
    height: size,
    transform: `scale(${pulse})`,
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    ...containerStyle, // Allow user to override default centering etc.
  };
 
  const svgViewBox = `0 0 ${size} ${size}`;
  const svgStyle: React.CSSProperties = {
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    transform: "rotate(-90deg)", // Start arc from the top
    transformOrigin: "center center",
  };
 
  const percentageTextStyle: React.CSSProperties = {
    position: "absolute", // Ensure it's centered within the main container
    top: "50%",
    left: "50%",
    transform: "translate(-50%, -50%)",
    fontSize: textSize,
    fontWeight: textFontWeight,
    color: textColor,
    zIndex: 2, // Above SVG elements
    ...textStyle,
  };
 
  return (
    <div style={mainContainerStyle}>
      {/* Background circle */}
      {showTrack && (
        <svg width="100%" height="100%" viewBox={svgViewBox} style={svgStyle}>
          <circle
            cx={size / 2}
            cy={size / 2}
            r={actualRadius}
            fill="none"
            stroke={trackColor}
            strokeWidth={trackStrokeWidth}
          />
        </svg>
      )}
 
      {/* Progress circle */}
      <svg width="100%" height="100%" viewBox={svgViewBox} style={svgStyle}>
        <defs>
          {progressColorMode === "gradient" && (
            <linearGradient
              id="progressGradient"
              x1="0%"
              y1="0%"
              x2="100%"
              y2="0%"
            >
              <stop offset="0%" stopColor={progressGradientStartColor} />
              <stop offset="100%" stopColor={progressGradientEndColor} />
            </linearGradient>
          )}
        </defs>
        <circle
          cx={size / 2}
          cy={size / 2}
          r={actualRadius}
          fill="none"
          stroke={
            progressColorMode === "gradient"
              ? "url(#progressGradient)"
              : progressSolidColor
          }
          strokeWidth={progressStrokeWidth}
          strokeDasharray={circumference}
          strokeDashoffset={strokeDashoffset}
          strokeLinecap={progressStrokeLinecap}
        />
      </svg>
 
      {/* Percentage text */}
      {showPercentageText && (
        <div style={percentageTextStyle}>{Math.round(currentProgress)}%</div>
      )}
    </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 CircularLoader component is added to your project (either via CLI or Manually), you can integrate it into your Remotion project by importing it and defining a Composition.

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 React from "react";
import { Player } from "@remotion/player";
 
import CircularLoader, {
  CircularProgressProps,
} from "../components/circular-loader";
 
// This Composition component will pass props to the new CircularLoader
interface CircularLoaderCompositionProps {
  loaderProps: CircularProgressProps; // Changed to avoid confusion with the old name
}
 
const CircularLoaderComposition: React.FC<CircularLoaderCompositionProps> = ({
  loaderProps,
}) => {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <CircularLoader {...loaderProps} />
    </div>
  );
};
 
export function CircularLoaderDemo() {
  const loaderPropsForDemo = React.useMemo<CircularProgressProps>(
    () => ({
      size: 250,
      progressSource: "time",
      durationInFrames: 120, // 4 seconds at 30fps
      loopProgress: true,
      // Track
      showTrack: true,
      trackColor: "rgba(100, 100, 100, 0.3)",
      trackStrokeWidth: 10,
      // Progress Arc
      progressStrokeWidth: 12,
      progressStrokeLinecap: "round",
      progressColorMode: "solid",
      progressSolidColor: "var(--primary)",
      // Pulse
      enablePulse: true,
      pulseMagnitude: 0.03,
      pulseSpeed: 0,
      // Text
      showPercentageText: true,
      textColor: "var(--primary)",
      textSize: "3rem",
      textFontWeight: "600",
    }),
    []
  );
 
  const playerDurationInFrames = 120; // Match durationInFrames for a full cycle example
 
  return (
    <Player
      component={CircularLoaderComposition}
      inputProps={{ loaderProps: loaderPropsForDemo }} // Pass the new props object
      durationInFrames={playerDurationInFrames}
      compositionWidth={640}
      compositionHeight={480} // Increased height for better viewing
      fps={30}
      style={{
        width: "100%",
        height: "100%",
      }}
      controls
      loop
    />
  );
}

Define a Composition

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

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

import CircularLoader from "@/components/clippkit/circular-loader"; // Assuming you placed it in src/components/clippkit

export const MyComposition = () => {
  return (
    <Composition
      id="CircularLoaderScene"
      component={CircularLoader}
      durationInFrames={150} // Example: 5 seconds at 30fps
      fps={30}
      width={1280}
      height={720}
      defaultProps={{
        loadingText: "Generating Video...",
        circleColor: "#1E90FF", // DodgerBlue
        textColor: "#333333",
        size: 100, // Diameter of the loader
      }}
    />
  );
};

API Reference

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

PropTypeDefaultDescription
loadingTextstring"Loading..."The text displayed in the center of the loader.
circleColorstring"var(--foreground)"The color of the spinning part of the circular loader.
textColorstring"var(--foreground)"The color of the loading text.
sizenumber100The diameter of the circular loader in pixels.
strokeWidthnumbersize / 10The thickness of the spinning circle.
trackColorstring"var(--muted)"The color of the track or background of the circular loader.
containerStyleReact.CSSProperties{}Custom styles for the main container div.
hideTextbooleanfalseControls the visibility of the loading text.
speednumber1Multiplier for the animation speed. Default is 1 (1 rotation per second for a typical 30fps setup).

On this page