Back to field notes
Playbooks 7 min read

The best way to record testimonials in the browser in 2026

Native browser recording has been good enough for production-quality testimonials for a while now. Here's what makes it work, what breaks it on mobile, and why it outperforms every app-download or screen-share alternative for customer completion rates.

TL;DR

The MediaRecorder API works natively in Chrome, Safari 14.1+, Firefox, and Edge — no plugin, no app download, no customer account required. Browser-based recording outperforms every alternative for completion rates because the path from 'click link' to 'recording started' is under ten seconds. The two things that kill it are using the wrong input method on iOS and not handling chunked uploads on flaky connections.

Before 2020, browser-based video recording was a workaround — fragile, codec-inconsistent, and likely to break on Safari in ways that were hard to diagnose. Teams building testimonial flows defaulted to asking customers to record with Loom or their phone's camera app and upload the file.

That approach has a completion problem. Every step between "I clicked the link" and "I recorded something" is a step where someone talks themselves out of it. Download Loom, create an account, record, export, find the file, come back to the form, upload. Most people who intended to help simply don't finish.

The browser has been good enough to skip all of that for a few years now. Here's what the actual implementation looks like, what breaks on mobile, and how to handle the edge cases that matter.

What the MediaRecorder API actually does#

MediaRecorder is a browser-native API that captures a stream from the user's camera and microphone and encodes it directly in the browser — no server required for the recording itself, no plugin, no installed software.

The basic flow is:

const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

const recorder = new MediaRecorder(stream);
const chunks = [];

recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: recorder.mimeType });
  // upload blob to your endpoint
};

recorder.start();
// ... user records ...
recorder.stop();

The customer sees a native browser permission prompt for camera and mic access — one click to allow. After that, recording is live. No second page, no waiting for a third-party script to load.

Browser support in 2026 is effectively complete: Chrome, Edge, Firefox, and Safari 14.1+ all implement it. The only gap worth checking is very old iOS Safari, which is a negligible fraction of real-world traffic.

The codec problem you'll hit on Safari#

Chrome defaults to VP8 or VP9 for video encoding. Safari uses H.264. If you create the Blob with a hardcoded MIME type, it'll fail silently on one or the other. The fix is to let the browser tell you what it prefers:

const getSupportedMimeType = () => {
  const types = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/webm',
    'video/mp4;codecs=h264,aac',
    'video/mp4',
  ];
  return types.find(type => MediaRecorder.isTypeSupported(type)) || '';
};

const recorder = new MediaRecorder(stream, {
  mimeType: getSupportedMimeType()
});

Store the recorder.mimeType alongside the upload so you transcode correctly on the server if you need a consistent output format. For most testimonial use cases — direct embed from the raw file — you don't need to transcode at all.

The iOS input trap#

The single most common implementation mistake is using <input type="file" accept="video/*"> as a shortcut on mobile.

On iOS, that input method hands control to the native camera app, which closes your browser context entirely. The customer records their testimonial, the camera app exits, and they're back on your page with no way to connect the recording to your flow — your consent UI never appeared, your upload handler never fired, and you have no video.

Use MediaRecorder directly, in the browser context, every time. On iOS Safari 14.1+, it works correctly. The browser stays open, the consent UI loads during upload, and the recording lands in your endpoint.

Handling the upload on flaky connections#

Recording is the easy part. The part that causes real data loss is the upload.

A customer on a phone in a building with inconsistent Wi-Fi records a great testimonial, hits submit, and the upload starts on a weak connection. Without retry logic, the upload fails silently, the customer thinks they're done, and the video is gone.

Two things prevent this.

Chunked upload. Don't wait until recording stops to start uploading. Set a timeslice on the MediaRecorder.start() call to emit data chunks every few seconds, and upload each chunk as it arrives:

recorder.start(3000); // emit a chunk every 3 seconds

recorder.ondataavailable = async (e) => {
  if (e.data.size > 0) {
    await uploadChunk(e.data, chunkIndex++);
  }
};

By the time the customer hits stop, most of the video is already on your server. Only the last few seconds need to land cleanly.

Exponential backoff on failed chunks. If a chunk upload fails, retry with increasing delays before giving up:

const uploadChunk = async (chunk, index, attempt = 0) => {
  try {
    await fetch('/api/upload-chunk', {
      method: 'POST',
      body: createChunkFormData(chunk, index)
    });
  } catch {
    if (attempt < 4) {
      await delay(Math.pow(2, attempt) * 500);
      return uploadChunk(chunk, index, attempt + 1);
    }
    // surface error to user after 4 failed attempts
  }
};

With chunked upload and retry logic, mobile drop-off from connection issues goes from a real problem to a rare edge case. At Proofly, about 8% of mobile recordings drop their connection mid-upload — with retries, only 0.4% fail to land.

What the recorder page should look like#

The technical implementation gets you a working recorder. What determines whether customers actually use it is the page around it.

The page should be nearly blank above the fold — three prompts, a record button, nothing else. Marketing copy, product explanations, and founder headshots all reduce completion. When Proofly tested adding a "Hi [Name]!" personalization header, completion dropped 11%. Customers read it as automation and hesitated. The page that converts doesn't try to sell anything.

After recording stops, show a short playback before the upload begins. It sounds like extra friction but it isn't — customers who can review their recording and choose to retake submit with more confidence, and the ones who would have submitted a broken recording catch it before you do. A thirty-second preview step saves a lot of "sorry, my mic wasn't on" follow-ups.

The consent checkbox should be visible and specific — not buried in a footer link, not handled by a "by submitting you agree" line. A checkbox that names the publishing surfaces (website, social, ads) with the upload button disabled until it's checked. One extra click is the right trade for the conversation it prevents.

When to use a tool versus building your own#

The implementation above is achievable in a weekend for a developer comfortable with browser APIs. Whether it's worth building depends on how much you expect to run testimonial collection as an ongoing process versus a one-off push.

Building your own makes sense if you need deep control over the UI or are building something testimonial-adjacent — customer interviews, async video feedback, onboarding recordings. The core logic is not complicated, and owning it means you can change anything without waiting on a tool's roadmap.

Using a tool like Proofly makes sense if you want the chunked upload, consent management, Wall of Love embed, and captions handled already — so the afternoon goes toward collecting testimonials rather than building the infrastructure to collect them. The free Sketch plan covers five videos, which is enough to validate whether the format moves your numbers before deciding to invest further.

The choice isn't really about capability. Most developers can build a working recorder. It's about whether the reliability engineering and edge case handling — iOS quirks, connection recovery, consent logging — is where you want to spend time right now.

The one-page summary for your implementation#

Start with getUserMedia to capture the stream. Initialize MediaRecorder with the supported MIME type rather than a hardcoded one. Start chunked upload in ondataavailable with exponential backoff on failures. Never use <input type="file"> on mobile. Add a 3-second countdown, a preview after recording, and a consent checkbox before the upload button activates.

The page around that recorder should be nearly empty — three prompts, one button, one checkbox. Every other element is something a customer will use as a reason to close the tab and get back to their day.

Frequently asked

Quick answers

What browsers support in-browser recording for testimonials?+

Chrome, Edge, Firefox, and Safari 14.1 and later all support the MediaRecorder API. That covers effectively all active desktop browsers and all modern mobile browsers. The one caveat is Safari on iOS before version 14.1 — but that's a very small fraction of traffic in 2026. You're safe to build for MediaRecorder without a fallback for nearly all users.

Is the video quality good enough for a landing page?+

Yes, with some nuance. The MediaRecorder API captures at whatever resolution the device camera supports — on a modern phone that's typically 720p or 1080p. The compression codec varies by browser (Chrome defaults to VP8, Safari uses H.264), but both produce files that look sharp on a landing page embed. The bigger quality factor is lighting, not codec. A customer recording in natural light on a three-year-old phone looks better than a customer recording in a dim room on a new MacBook.

Can I add a timer or countdown before recording starts?+

Yes, and you should. A 3-second countdown before the MediaRecorder.start() call gives customers a moment to settle before the camera is actually on. It's a small UX detail that meaningfully improves the quality of what you get — fewer recordings that start mid-sentence or with the customer still adjusting their position.