Building a Browser-Based Slideshow Video Editor: Leveraging Modern Frontend Technologies

by | Mar 9, 2026 | Articles, Projects | 0 comments

How CSS3, Canvas API, and MediaRecorder enable powerful video creation without server-side processing


For years, video editing and export capabilities were the exclusive domain of desktop applications or server-side processing pipelines. The conventional wisdom was that browsers simply couldn’t handle the computational demands of video manipulation. Today, that assumption is fundamentally wrong.

In this post, I’ll walk through building a complete slideshow video creator that runs entirely in the browser—no server required for video processing. We’ll explore how modern frontend technologies like CSS3 animations, Canvas API, and MediaRecorder can be combined to create a powerful, production-ready video creation tool.

The Architecture: Client-Side Video Production

Our application follows a straightforward but powerful architecture:

flowchart TD
    A[User Input<br/>Images/Text/Audio] --> B
    
    subgraph B [State Management - Zustand]
        B1[Upload Store]
        B2[Settings Store]
        B3[Preview Store]
    end
    
    B --> C[Real-time Preview<br/>CSS3 + React]
    
    C --> D[Video Export<br/>Canvas + MediaRecorder]
    
    D --> E[Downloadable<br/>Video File]
    
    B -.-> C
    C -.-> B
    
    style A fill:#e1f5fe,stroke:#01579b
    style B fill:#fff3e0,stroke:#ff6f00
    style C fill:#f3e5f5,stroke:#4a148c
    style D fill:#e8f5e8,stroke:#1b5e20
    style E fill:#fff9c4,stroke:#f57f17
flowchart LR
    subgraph Input [Input Layer]
        I1[Images]
        I2[Text]
        I3[Audio]
    end
    
    subgraph State [State Management - Zustand]
        S1[(Upload Store)]
        S2[(Settings Store)]
        S3[(Preview Store)]
    end
    
    subgraph Preview [Preview Layer]
        P1[React Components]
        P2[CSS3 Styling]
        P3[Real-time Render]
    end
    
    subgraph Export [Export Layer]
        E1[Canvas Drawing]
        E2[MediaRecorder API]
        E3[Video Encoding]
    end
    
    Output[Downloadable MP4/WebM]
    
    Input --> State
    State <--> Preview
    Preview --> Export
    Export --> Output
    
    style Input fill:#e3f2fd
    style State fill:#fff3e0
    style Preview fill:#f3e5f5
    style Export fill:#e8f5e8
    style Output fill:#fff9c4

The key insight is that we use two parallel rendering paths:

  1. Preview Mode: Uses CSS3 animations for smooth, real-time preview
  2. Export Mode: Uses Canvas API for frame-by-frame video capture

This separation allows us to optimize for different goals—interactivity during editing, and precise control during export.

Technology Stack

Let’s break down the core technologies and why they matter:

Next.js 16 with React 19

The App Router provides excellent developer experience, while React 19’s concurrent features help maintain smooth UI updates during heavy rendering operations.

Zustand for State Management

Video editing requires managing complex state—multiple slides, effects, audio tracks, timing, and export settings. Zustand provides a simple, performant solution:

interface SlideshowState {
  slides: Slide[];
  selectedSlideId: string | null;
  isPlaying: boolean;
  aspectRatio: AspectRatio;
  audioSettings: AudioSettings;

  // Actions
  addSlide: (slide: Omit<Slide, 'id'>) => void;
  updateSlide: (id: string, updates: Partial<Slide>) => void;
  applyToAllSlides: (updates: Partial<Slide>) => void;
}

The key pattern here is immutability with selective updates—essential for preventing unnecessary re-renders during playback.

Tailwind CSS for Styling

Beyond simple styling, Tailwind’s utility-first approach shines when building dynamic UIs. We can conditionally apply classes based on state without complex CSS-in-JS solutions.

The Power of CSS3 Animations

Here’s where things get interesting. For the preview, we leverage CSS3 animations extensively. Why? Because CSS animations are:

  1. GPU-accelerated: Transform and opacity changes run on the GPU
  2. Frame-perfect: The browser handles timing, ensuring smooth 60fps
  3. Declarative: Easy to reason about and modify

Ken Burns Effect Implementation

The Ken Burns effect—that subtle pan and zoom that makes static images feel cinematic—is a perfect example:

@keyframes kenBurnsZoomInContinuous {
  0% { transform: scale(1.05); }
  100% { transform: scale(1.25); }
}

@keyframes kenBurnsPanLeftContinuous {
  0% { transform: scale(1.1) translateX(5%); }
  100% { transform: scale(1.1) translateX(-5%); }
}

.ken-burns-active img {
  width: 110%;
  height: 110%;
  object-fit: cover;
  margin: -5%;
}

The animation duration is dynamically set via CSS custom properties:

<div
  style={{
    '--kb-duration': `${slide.duration}s`,
    animation: `kenBurnsZoomInContinuous var(--kb-duration) ease-out forwards`,
  }}
>

This approach allows each slide to have its own animation timing while using the same CSS keyframes.

Transition Effects

We implemented 21 different transition styles, from simple fades to complex 3D flips:

@keyframes flipXPreview {
  from { transform: perspective(800px) rotateX(90deg); opacity: 0; }
  to { transform: perspective(800px) rotateX(0deg); opacity: 1; }
}

@keyframes wipeLeftPreview {
  0% { clip-path: inset(0 100% 0 0); }
  100% { clip-path: inset(0 0 0 0); }
}

The clip-path property enables sophisticated wipe transitions that were previously only possible with canvas masking.

Overlay Effects: Particle Systems in CSS

One of the most impressive demonstrations of CSS capabilities is our particle effect system. Effects like snow, rain, confetti, and sparkles are rendered entirely with CSS:

.snow-container .snowflake {
  position: absolute;
  top: -10px;
  width: 8px;
  height: 8px;
  background: white;
  border-radius: 50%;
  animation: snowfall linear infinite;
}

@keyframes snowfall {
  0% { transform: translateY(-10px) rotate(0deg); opacity: 0; }
  10% { opacity: 0.8; }
  100% { transform: translateY(100vh) rotate(360deg); opacity: 0.8; }
}

Each particle is a DOM element, dynamically generated based on intensity settings. While this might seem inefficient, modern browsers handle hundreds of animated elements smoothly thanks to compositing optimizations.

The Canvas API: Frame-by-Frame Control

While CSS excels at preview, video export requires precise control over every frame. This is where the Canvas API becomes essential.

The Export Pipeline

async function exportVideo() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const stream = canvas.captureStream(fps);

  const mediaRecorder = new MediaRecorder(stream, {
    mimeType: 'video/webm;codecs=vp9',
    videoBitsPerSecond: 8000000,
  });

  mediaRecorder.start();

  // Render each frame
  for (const slide of slides) {
    for (let frame = 0; frame < slide.duration * fps; frame++) {
      renderFrame(ctx, slide, frame);
      await new Promise(r => setTimeout(r, 1000 / fps));
    }
  }

  mediaRecorder.stop();
}

Replicating CSS Effects on Canvas

The challenge is translating CSS effects to canvas. Here’s how we handle overlay effects:

function renderOverlayEffects(ctx, effects, width, height, frameNumber) {
  effects.forEach(effect => {
    switch (effect.type) {
      case 'snow': {
        const count = Math.floor(effect.intensity / 2);
        for (let i = 0; i < count; i++) {
          const seed = seededRandom(i);
          const x = seed * width;
          const y = ((frameNumber * 2 + seed * height * 10) % (height + 20)) - 10;

          ctx.beginPath();
          ctx.arc(x, y, 4, 0, Math.PI * 2);
          ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
          ctx.fill();
        }
        break;
      }
      case 'vignette': {
        const gradient = ctx.createRadialGradient(
          width / 2, height / 2, 0,
          width / 2, height / 2, Math.max(width, height) * 0.7
        );
        gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
        gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)');
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, width, height);
        break;
      }
    }
  });
}

The key insight is using seeded random numbers for consistent particle positions across frames:

function seededRandom(seed: number) {
  const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453;
  return x - Math.floor(x);
}

This ensures that if we re-render the same frame, particles appear in identical positions—essential for video consistency.

Ken Burns on Canvas

Translating the Ken Burns effect to canvas requires interpolating transformation values across frames:

function applyKenBurns(ctx, slide, progress, width, height) {
  const intensity = slide.kenBurns.intensity / 5;
  let scale = 1;
  let panX = 0;
  let panY = 0;

  switch (slide.kenBurns.type) {
    case 'zoomIn':
      scale = 1 + progress * 0.2 * intensity;
      break;
    case 'panLeft':
      panX = -progress * 10 * intensity;
      scale = 1.1;
      break;
    case 'zoomInPanLeft':
      scale = 1 + progress * 0.15 * intensity;
      panX = -progress * 5 * intensity;
      break;
  }

  ctx.translate(width / 2, height / 2);
  ctx.scale(scale, scale);
  ctx.translate(-width / 2 + panX, -height / 2 + panY);
}

Audio Integration with Web Audio API

A video editor isn’t complete without audio. The Web Audio API allows us to:

  1. Load and decode audio files
  2. Synchronize playback with video
  3. Capture audio during export
async function setupAudioExport(stream, audioSettings) {
  const audioContext = new AudioContext();
  const audioElement = new Audio(audioSettings.track.url);
  audioElement.crossOrigin = 'anonymous';

  const source = audioContext.createMediaElementSource(audioElement);
  const destination = audioContext.createMediaStreamDestination();

  source.connect(destination);

  // Add audio track to video stream
  destination.stream.getAudioTracks().forEach(track => {
    stream.addTrack(track);
  });

  await audioElement.play();
}

This approach combines the audio stream with our canvas video stream, resulting in a complete video file with synchronized audio.

Intelligent Image Fitting

One challenge in video creation is handling images with different aspect ratios. We implemented intelligent detection:

function detectOptimalImageFit(
  imageWidth: number,
  imageHeight: number,
  containerRatio: AspectRatio
) {
  const imageRatio = imageWidth / imageHeight;
  const targetRatio = parseAspectRatio(containerRatio);

  const ratioDiff = Math.abs(imageRatio - targetRatio) / targetRatio;

  if (ratioDiff > 0.3) {
    // Image aspect ratio differs significantly
    return {
      fit: 'cover',
      scale: 1.1, // Slight zoom to prevent edge gaps
      panX: 0,
      panY: 0,
    };
  }

  return { fit: 'cover', scale: 1, panX: 0, panY: 0 };
}

This automatically adjusts the image to fit well within the chosen aspect ratio.

Blurred Background Effect

For images that don’t fill the frame, we use a blurred version of the image as background:

// In preview (CSS)
<div className="absolute inset-0 scale-125 blur-xl opacity-80">
  <img src={slide.imageUrl} className="w-full h-full object-cover" />
</div>

// In export (Canvas)
if (slide.useBlurredBackground) {
  ctx.save();
  ctx.filter = 'blur(20px)';
  ctx.globalAlpha = 0.8;
  ctx.scale(1.3, 1.3);
  ctx.drawImage(img, -width * 0.15, -height * 0.15, width * 1.3, height * 1.3);
  ctx.restore();
}

This creates a professional look without revealing black bars.

Performance Considerations

Frame Rate Management

During export, we control frame rate explicitly:

const fps = 30;
for (let frame = 0; frame < totalFrames; frame++) {
  renderFrame(ctx, frame);
  await new Promise(r => setTimeout(r, 1000 / fps));
}

This ensures consistent timing regardless of actual render speed.

Memory Management

For long videos, we need to be mindful of memory:

// Use smaller images for timeline thumbnails
const thumbnailUrl = await createThumbnail(imageUrl, 320, 180);

// Clear references after export
mediaRecorder.onstop = () => {
  chunksRef.current = [];
  setVideoBlob(new Blob(chunks));
};

Progressive Loading

We load images on demand during export:

for (const slide of slides) {
  const img = new Image();
  img.crossOrigin = 'anonymous';

  await new Promise(resolve => {
    img.onload = resolve;
    img.src = slide.imageUrl;
  });

  // Render frames...
}

The “Apply to All” Pattern

A common need in video editing is applying changes to multiple slides. We implemented this as a store action:

applyToAllSlides: (updates) => {
  set((state) => ({
    slides: state.slides.map(slide => ({ ...slide, ...updates })),
  }));
}

This allows users to quickly apply effects, transitions, or styling across their entire project.

Lessons Learned

1. CSS is Surprisingly Powerful

For preview and real-time effects, CSS animations outperform manual canvas rendering. The browser’s rendering engine is highly optimized.

2. Canvas Requires Explicit State

Unlike CSS’s declarative model, canvas requires managing animation state explicitly. A seeded random function is essential for reproducible effects.

3. MediaRecorder Has Limitations

  • Only WebM format is widely supported for encoding
  • Frame-perfect timing requires careful management
  • Audio sync can be tricky—test across browsers

4. Performance Varies by Effect

Particle effects with hundreds of elements work fine in CSS preview but can slow canvas export. Consider reducing complexity during export.

Conclusion

Building this slideshow video creator demonstrated that modern frontend technologies are more than capable of handling complex video creation tasks. By combining:

  • CSS3 for smooth, GPU-accelerated previews
  • Canvas API for precise frame-by-frame rendering
  • MediaRecorder for video encoding
  • Web Audio API for audio integration

We created a complete video production pipeline that runs entirely in the browser.

The implications are significant: video creation tools can now be delivered as web applications, accessible from any device without installation. This opens up possibilities for content creation platforms, marketing tools, and creative applications that were previously desktop-only.

The full source code is available in the repository. I encourage you to explore the implementation and consider how these techniques might apply to your own projects.

https://github.com/kalyanraju/slideshowCreator

What frontend technologies have you used for unexpected applications? Share your experiences in the comments.

Written by

Related Posts

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *