Custom Webcam Component for Retool Web Apps: Capture, Download, and Copy Images Effortlessly!

Hey Retool Community! :wave:

We’re excited to share a custom Webcam component that brings image capture functionality right into your Retool web apps! Since Retool doesn’t have a native image capture option, this component seamlessly fills that gap.

:tada: Key Features:

  • Capture Images: Easily capture images directly from your device's webcam.
  • Download Captured Image: Save the image to your device with a single click.
  • Copy Base64 Data: Quickly copy the Base64 representation of the image—perfect for data storage or processing.
  • Clear Captured Image: Reset the capture area effortlessly to take a new photo without refreshing the page.

** :camera_flash: Here’s How It Works:**

We built this component using React with the react-webcam library and integrated it smoothly into Retool using the Retool Custom Component SDK. The image is captured as a Base64 string, making it easy to store or send to APIs.

The component manages state to handle the captured image, Base64 data, and interactions like copying to clipboard or downloading the image. The layout is styled for a user-friendly experience with buttons to capture, download, copy, and clear the image.

  1. Image Capture Interface: The capture button activates the webcam feed.

  2. Captured Image Preview: Get a live preview of the captured image along with options to download or copy the Base64 data.

  3. Base64 Copy in Action: Easily copy the Base64 string for seamless integration into your workflow.

:bulb: Use Cases:
This custom component is ideal for:

  • ID verification
  • User profile setup
  • Any scenario where you need to capture and process images within your Retool app!

We’d love to hear your feedback or help you get started! Drop your questions below, and happy building! :rocket:

6 Likes

Thanks for creating and sharing this, it looks like exactly what I’m after.

I just tried to use this but I've run into a camera permissions issue for the custom component which I’m not sure how to change. I’ve got the component to also print out the devices and the web camera appears in the list. I'm brand new to custom components but this is what I have so far (following this format - tryretool/custom-component-collection-template)

import React, { useRef, useCallback } from "react";
import Webcam from "react-webcam";
import { Retool } from "@tryretool/custom-component-support";

export const WebcamCapture: React.FC = () => {
 const webcamRef = useRef<Webcam>(null);
 const [imgSrc, setImgSrc] = Retool.useStateString({ name: "imageSrc" });

 const capture = useCallback(() => {
   const imageSrc = webcamRef.current?.getScreenshot();
   if (imageSrc) setImgSrc(imageSrc);
 }, [webcamRef, setImgSrc]);

 // const [deviceId, setDeviceId] = React.useState({});
 const [deviceId, setDeviceId] = React.useState<string>("");
 const [devices, setDevices] = React.useState([]);

 const handleDevices = React.useCallback(
   mediaDevices =>
     setDevices(mediaDevices.filter(({ kind }) => kind === "videoinput")),
   [setDevices]
 );

 React.useEffect(
   () => {
     navigator.mediaDevices.enumerateDevices().then(handleDevices);
   },
   [handleDevices]
 );

 return (
   <div>
     <Webcam
       audio={false}
       ref={webcamRef}
       screenshotFormat="image/jpeg"
       videoConstraints={{ deviceId: deviceId }}
       width={340}
       height={280}
     />
     <button onClick={capture}>Capture</button>
     <div> Potential cameras: </div>
     <ul>
       {devices.map((device, key) => (
         <li key={device.deviceId}>
           {device.label || `Device ${key + 1}`}
         </li>
       ))}
     </ul>
   </div>
 );
};

Then this is what it shows

While trying to fix this is used the retool scanner component and it displayed the web camera stream just fine.

Do you know if there are certain settings I should update? Alternatively, should I deploy this differently?

1 Like

Thanks for trying it out and for sharing the details.

If the Retool Scanner component is able to access and display your webcam, but the custom component cannot, then it’s likely related to how camera permissions are being requested within the custom component environment rather than an issue with the webcam itself.

A couple of things I'd check:

  • Make sure the custom component is actually triggering a getUserMedia() request before rendering the webcam. In some environments, enumerateDevices() can list available cameras, but camera access won't be granted until navigator.mediaDevices.getUserMedia({ video: true }) is called.
  • Check your browser's camera permissions for the Retool app URL and verify camera access hasn't been blocked.
  • Open the browser console and look for any permission or media-related errors.
  • Compare the Scanner component implementation against the custom component to see whether it's handling permissions differently.

Also, can you confirm whether you're deploying this through the Retool Custom Component Library workflow described in the gallery example? The webcam component in the gallery should work when deployed following those steps:

If possible, could you also share any console errors you're seeing? That would help narrow down whether this is a permissions issue, a browser restriction, or something specific to the custom component runtime.

Thanks for the prompt reply.

I tried adding the code you suggested and I believe that reveals the issue.

I got
Camera permission: denied
Error: SecurityError: Invalid security origin

I believe this is because of how we are hosting retool. I'll talk to our IT and see if we can fix it so it allows custom components to access the camera as well as inbuilt retool components :crossed_fingers:

1 Like

That definitely points to a hosting/configuration issue rather than the component itself.

SecurityError: Invalid security origin usually means the custom component doesn't have permission to access getUserMedia(), often due to HTTPS, CSP, iframe permissions, or other self-hosted Retool configuration.

Hopefully your IT team can get it sorted. If you find the root cause, please share it here—it could help others running into the same issue.

1 Like