UI/Components/Audio waveforms/Circular Waveform

Circular Waveform

A customizable circular audio waveform visualization component.

Animate a circular waveform, often used for audio visualizations.

0:00 / 0:10

Installation

CLI

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

Manual

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

/**
 * Free Remotion Template Component
 * ---------------------------------
 * This template is free to use in your projects!
 * Credit appreciated but not required.
 *
 * Created by the team at https://www.reactvideoeditor.com
 *
 * Happy coding and building amazing videos! 🎉
 */
 
"use client";
 
import React from "react";
import {
  MediaUtilsAudioData,
  visualizeAudioWaveform,
} from "@remotion/media-utils";
import { random, useCurrentFrame, useVideoConfig } from "remotion";
 
interface CircularWaveformProps {
  audioData?: MediaUtilsAudioData | null;
  barCount?: number;
  barWidth?: number;
  barColor?: string;
  waveAmplitude?: number;
  radius?: number;
  centerOffset?: { x?: number; y?: number };
  containerStyle?: React.CSSProperties;
  barStyle?: React.CSSProperties;
  height?: string | number;
  width?: string | number;
  barMinHeight?: number;
  strokeLinecap?: "butt" | "round" | "square";
  transitionDuration?: string;
  transitionTimingFunction?: string;
  rotationOffset?: number;
  growOutwardsOnly?: boolean;
}
 
export function CircularWaveform({
  audioData,
  barCount = 60,
  barWidth = 5,
  barColor = "var(--foreground)",
  waveAmplitude = 50,
  radius = 100,
  centerOffset = { x: 0, y: 0 },
  containerStyle,
  barStyle,
  height: propHeight,
  width: propWidth,
  barMinHeight = 5,
  strokeLinecap = "butt",
  transitionDuration = "0.05s",
  transitionTimingFunction = "ease-out",
  rotationOffset = 0,
  growOutwardsOnly = false,
}: CircularWaveformProps) {
  const frame = useCurrentFrame();
  const { width: videoWidth, height: videoHeight, fps } = useVideoConfig();
 
  const finalWidth = propWidth ?? videoWidth;
  const finalHeight = propHeight ?? videoHeight;
 
  const centerX =
    (typeof finalWidth === "number"
      ? finalWidth / 2
      : parseFloat(String(finalWidth).replace("px", "")) / 2) +
    (centerOffset.x ?? 0);
  const centerY =
    (typeof finalHeight === "number"
      ? finalHeight / 2
      : parseFloat(String(finalHeight).replace("px", "")) / 2) +
    (centerOffset.y ?? 0);
 
  const waveformSamples = audioData
    ? visualizeAudioWaveform({
        fps,
        frame,
        audioData,
        numberOfSamples: barCount,
        windowInSeconds: 1 / fps,
      })
    : Array(barCount)
        .fill(0)
        .map((_, i) => {
          const seed = i * 1000;
          return (
            Math.max(
              0.1,
              Math.abs(Math.sin(frame / 10 + i / (barCount / (2 * Math.PI)))) +
                random(seed) * 0.3
            ) *
              0.5 +
            0.25
          );
        });
 
  const bars = waveformSamples.map((sample, i) => {
    const angleRad =
      (i / barCount) * 2 * Math.PI + (rotationOffset * Math.PI) / 180;
    const dynamicHeight = Math.max(barMinHeight, sample * waveAmplitude);
 
    let startRadius: number;
    let endRadius: number;
 
    if (growOutwardsOnly) {
      startRadius = radius;
      endRadius = radius + dynamicHeight;
    } else {
      startRadius = radius - dynamicHeight / 2;
      endRadius = radius + dynamicHeight / 2;
    }
 
    if (startRadius < 0) {
      endRadius += Math.abs(startRadius);
      startRadius = 0;
    }
 
    const maxAllowedRadius = Math.min(centerX, centerY);
    if (endRadius > maxAllowedRadius) {
      endRadius = maxAllowedRadius;
    }
    if (startRadius > endRadius) {
      startRadius = endRadius - barMinHeight > 0 ? endRadius - barMinHeight : 0;
    }
 
    const finalX1 = centerX + startRadius * Math.cos(angleRad);
    const finalY1 = centerY + startRadius * Math.sin(angleRad);
    const finalX2 = centerX + endRadius * Math.cos(angleRad);
    const finalY2 = centerY + endRadius * Math.sin(angleRad);
 
    return {
      x1: finalX1,
      y1: finalY1,
      x2: finalX2,
      y2: finalY2,
      height: endRadius - startRadius,
    };
  });
 
  return (
    <div
      style={{
        width: finalWidth,
        height: finalHeight,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        overflow: "hidden",
        backgroundColor: "transparent",
        position: "relative",
        ...containerStyle,
      }}
    >
      <svg width="100%" height="100%" style={{ overflow: "visible" }}>
        {bars.map((bar, i) => (
          <line
            key={i}
            x1={bar.x1}
            y1={bar.y1}
            x2={bar.x2}
            y2={bar.y2}
            stroke={barColor}
            strokeWidth={barWidth}
            strokeLinecap={strokeLinecap}
            style={{
              transitionProperty: "all",
              transitionDuration: transitionDuration,
              transitionTimingFunction: transitionTimingFunction,
              ...barStyle,
            }}
          />
        ))}
      </svg>
    </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 CircularWaveform 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 React
import { MediaUtilsAudioData, useAudioData } from "@remotion/media-utils";
import { Player } from "@remotion/player";
import { Audio } from "remotion"; // Import Audio
 
import CircularWaveform from "../components/circular-waveform";
 
// Media source for the demo
const MEDIA_SRC =
  "https://rwxrdxvxndclnqvznxfj.supabase.co/storage/v1/object/public/sounds//moon-landing.mp3";
 
// Define props for the new composition component
interface AudioCircularWaveformCompositionProps {
  circularWaveformProps: Omit<
    React.ComponentProps<typeof CircularWaveform>,
    "audioData"
  > & { audioData?: MediaUtilsAudioData | null };
  mediaSrc: string;
}
 
// New component combining CircularWaveform and Audio
const AudioCircularWaveformComposition: React.FC<
  AudioCircularWaveformCompositionProps
> = ({ circularWaveformProps, mediaSrc }) => {
  return (
    <>
      <CircularWaveform {...circularWaveformProps} />
      <Audio src={mediaSrc} />
    </>
  );
};
 
export function CircularWaveformDemo() {
  const audioData = useAudioData(MEDIA_SRC);
 
  const circularWaveformProps = React.useMemo(
    () => ({
      barCount: 180,
      barWidth: 2,
      waveAmplitude: 100,
      radius: 80,
      audioData,
      strokeLinecap: "round" as const,
      barMinHeight: 2,
      transitionDuration: "0.1s",
      transitionTimingFunction: "ease-in-out",
      rotationOffset: 45,
      barColor: "var(--foreground)",
      growOutwardsOnly: true,
    }),
    [audioData]
  );
 
  // Calculate duration in frames for the player for better readability
  const playerDurationInFrames = audioData
    ? Math.floor(audioData.durationInSeconds * 30)
    : 300;
 
  return (
    <Player
      component={AudioCircularWaveformComposition} // Use the new component
      inputProps={{ circularWaveformProps, mediaSrc: MEDIA_SRC }} // Pass props directly
      durationInFrames={playerDurationInFrames} // e.g., 10 seconds at 30fps
      compositionWidth={640}
      compositionHeight={360} // 16:9 aspect ratio
      fps={30}
      style={{
        width: "100%",
        height: "100%", // Player will scale to fit its container
        backgroundColor: "transparent",
      }}
      controls // Show player controls
      loop
    />
  );
}

Define a Composition

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

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

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

export default function MyVideoComposition() {
  const waveformProps = {
    barCount: 70,
    barColor: "#3498db",
    waveAmplitude: 40,
    waveSpeed: 12,
    radius: 80,
  };

  return (
    <Player
      component={CircularWaveform}
      inputProps={waveformProps}
      durationInFrames={300}
      compositionWidth={640}
      compositionHeight={360}
      fps={30}
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "#2c3e50",
      }}
      autoPlay
      controls
      loop
    />
  );
}

API Reference

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

PropTypeDefault ValueDescription
barCountnumber60Number of bars (lines) in the waveform.
barWidthnumber5Width (strokeWidth) of each bar in pixels.
barColorstring"white"Color of the bars. Accepts any valid CSS color value.
waveAmplitudenumber50Maximum length extension of the wave bars from the base radius.
waveSpeednumber10Speed of the wave animation. Higher value means slower animation.
randomnessnumber30Amount of random length variation applied to each bar. Set to 0 for no randomness.
radiusnumber100Radius of the base circle from which waveform bars extend.
centerOffset{ x?: number; y?: number }{ x: 0, y: 0 }Offsets the center of the circular waveform from the center of its container. Useful for precise positioning.
heightstring | numberVideo config heightExplicit height for the waveform container. Overrides video config height.
widthstring | numberVideo config widthExplicit width for the waveform container. Overrides video config width.
containerStyleReact.CSSProperties{}Custom CSS styles for the main container div.
barStyleReact.CSSProperties{}Custom CSS styles for individual bars (SVG <line> elements). Applied to each bar.

On this page