Getting an image from your users: a compatibility-first approach
Modern browsers increasingly offer direct access to a user’s camera, but support varies widely. Some browsers provide a full, inline video experience; others delegate capture to a separate app. And some devices simply have no camera at all. Building an image-capture feature that works everywhere means starting with the most universally supported method and layering enhancements on top.
Start with a simple file input
The most reliable approach is to ask the user for a pre-existing image file. A basic file input with an accept filter for images works on every platform:
<input type="file" accept="image/*" />
On desktop, this opens a standard file picker. On Chrome and Safari for iOS and Android, the user gets a choice of apps to source the image, including the camera or an existing file.
You read the selected data by listening for an onchange event and accessing the files property on the event target:
<input type="file" accept="image/*" id="file-input" />
<script>
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', (e) =>
doSomethingWithFiles(e.target.files),
);
</script>
Optionally, add the capture attribute to signal a preference for the camera:
<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />
The bare capture attribute lets the browser choose the camera. Values of "user" and "environment" prefer the front and rear cameras, respectively. This attribute is respected on Android and iOS but ignored on desktop. Note that on Android, setting capture will launch the camera app directly, removing the user's option to pick an existing picture.
The files property here is a FileList, which you can read in the usual way to get a File object—itself a Blob with extra name and lastModified properties.
Drag-and-drop and paste
To enrich the file input experience, add a drop target. Handle the drop event and retrieve files from dataTransfer.files:
<div id="target">You can drag an image file here</div>
<script>
const target = document.getElementById('target');
target.addEventListener('drop', (e) => {
e.stopPropagation();
e.preventDefault();
doSomethingWithFiles(e.dataTransfer.files);
});
target.addEventListener('dragover', (e) => {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
</script>
Signalling the expected action on dragover via the dropEffect property improves clarity. Drag-and-drop enjoys broad browser support.
Reading from the clipboard is equally straightforward, though the UX is trickier:
<textarea id="target">Paste an image here</textarea>
<script>
const target = document.getElementById('target');
target.addEventListener('paste', (e) => {
e.preventDefault();
doSomethingWithFiles(e.clipboardData.files);
});
</script>
For full cross-browser support, the event target must be selectable and editable—<textarea>, <input type="text">, or an element with contenteditable. This makes the feature awkward in contexts where the user shouldn’t type text; hiding an input element to handle the interaction can complicate accessibility.
Handling FileList
Most acquisition methods return a FileList. Though it resembles an array with numeric keys and a length, it lacks array methods and isn’t iterable. Use Array.from(fileList) to convert it if needed.
You can locate the first image file by inspecting MIME types:
<img id="output" />
<script>
const output = document.getElementById('output');
function doSomethingWithFiles(fileList) {
let file = null;
for (let i = 0; i < fileList.length; i++) {
if (fileList[i].type.match(/^image\//)) {
file = fileList[i];
break;
}
}
if (file !== null) {
output.src = URL.createObjectURL(file);
}
}
</script>
Once you have the file, you can draw it to a <canvas> for manipulation, download it locally, or upload it via fetch().
Progressing to live camera access
When your base upload path is solid, you can add direct interaction for browsers that support it. The WebRTC getUserMedia() API provides live access to a camera and microphone. Support is broad but not universal; notably, Safari 10 and lower lack it. Detection is straightforward:
const supported = 'mediaDevices' in navigator;
Calling getUserMedia() requires a constraints object describing preferred media—for camera data alone, video: true suffices. The resulting MediaStream can be attached to a <video> element for a live preview:
<video id="player" controls playsinline autoplay></video>
<script>
const player = document.getElementById('player');
const constraints = {
video: true,
};
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
player.srcObject = stream;
});
</script>
Capturing a snapshot
To capture a still image, you draw a frame from the video into a canvas:
<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
const player = document.getElementById('player');
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const captureButton = document.getElementById('capture');
const constraints = {
video: true,
};
captureButton.addEventListener('click', () => {
// Draw the video frame to the canvas.
context.drawImage(player, 0, 0, canvas.width, canvas.height);
});
// Attach the video stream to the video element and autoplay.
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
player.srcObject = stream;
});
</script>
The sequence: create a canvas for the frame, obtain the camera stream, attach it to a video element, and call drawImage() when you want a snapshot. The canvas data can then be uploaded, stored, or processed client-side.
Responsibility and cleanup
Release the camera
Stop using the camera as soon as it isn’t needed—this saves resources and builds user trust. Stop each video track on the stream:
<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
const player = document.getElementById('player');
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const captureButton = document.getElementById('capture');
const constraints = {
video: true,
};
captureButton.addEventListener('click', () => {
context.drawImage(player, 0, 0, canvas.width, canvas.height);
// Stop all video streams.
player.srcObject.getVideoTracks().forEach(track => track.stop());
});
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
// Attach the video stream to the video element and autoplay.
player.srcObject = stream;
});
</script>
Prompt for permission sparingly
Calling getUserMedia() on an ungranted site immediately triggers a permission prompt. Users often block these requests when the context isn’t clear. Ask for camera access only at the moment it's required. Once granted, subsequent calls won't re-prompt; a denial, however, is permanent until the user manually changes the site’s permission settings.
For WebRTC-based features, consider the adapter.js shim, which insulates your code from spec changes and vendor prefixes.



