Registry Index

Search the catalog

Type to search items across every indexed registry, or pick a page.

ComponentReactregistry:ui1 dependency

Camera Capture

camera-capture

A take-a-photo control that opens the device camera inline, shows a live preview, captures a still to a Blob, and — the part every hand-rolled version gets wrong — hands the camera back the moment it is done with it. Reach for it wherever a form needs a picture taken now rather than a file chosen from a gallery: a profile or avatar photo, ID and proof-of-address capture in a KYC or onboarding flow, a business card scanned into a CRM, stock and asset counts in a warehouse, site and installation records on a construction job, damage reports for a repair, an insurance claim or a delivery dispute, proof-of-delivery photos, receipt and invoice capture for expenses, meter readings, a VIN or serial plate, product photos for a marketplace listing, a whiteboard at the end of a meeting, and a check-in photo in a field-service or inspection app. Common asks it answers: "react camera component", "take photo in browser react", "getUserMedia react hook", "webcam capture component", "react webcam alternative", "capture image from video stream", "shadcn camera", "photo capture for form upload", "scan document with phone camera react", "front and back camera switch react", "useUserMedia hook", "canvas toBlob from video", "selfie capture component", "camera permission denied react", "camera light stays on after stopping". Official shadcn/ui has nothing for this and no combination of its parts reaches it: there is no camera, video, media or capture primitive anywhere in the library — getUserMedia, MediaStream, facingMode, srcObject, enumerateDevices, toBlob and even the string "camera" are each zero hits across all sixty-odd of its components. Distinct from the other image pieces in this registry, and composes with them rather than repeating them: file-dropzone takes a file that already exists, image-crop trims a picture after it has been obtained, image-zoom inspects one, upload-list shows what is in flight, and signature-pad is also a canvas but its input is a finger, not a lens. This one is the step that produces the image in the first place — pipe its blob straight into image-crop, or into a FormData with form.append("photo", photo.blob, "photo.jpg"). Five things separate it from the twenty-line version. First, stopping. Setting video.srcObject to null blanks the preview and leaves the tracks live, so the operating system's recording indicator stays lit over a camera nobody is watching; only track.stop() ends the capture, and it runs on unmount, on an explicit stop, when a photo is taken, and before any restart. On a phone the camera is also exclusive, so the release has to happen before the next getUserMedia and not after it, or switching cameras fails with NotReadableError on exactly the devices that have two cameras to switch between — and a stream that arrives after the component has gone is stopped on arrival, because nothing else on the page can reach it any more. Second, facing. { exact: "environment" } fails outright on any device without a rear camera, which is every laptop, and plain "environment" never fails and silently returns the front camera instead, so a document scanner written either way is broken: one refuses to run, the other photographs the user's face and files it as their passport page. This asks for the ideal and then reads the live track back, so facing reports the camera actually in use and facingFallback says when it is not the one requested — the answer neither constraint spelling gives you. Third, the black photograph. loadedmetadata publishes videoWidth and videoHeight, so a canvas sized from them looks right, but no frame has been decoded yet and drawImage paints nothing; the result is a correctly sized, entirely black JPEG that passes every check a caller is likely to write. Capture is gated on readyState, not on dimensions. Fourth, permissions. NotAllowedError means four unrelated things — an http origin, an iframe without allow="camera", a stored block, and a prompt closed without an answer — and only the stored block is worth sending somebody to their site settings for, so the other three say something true instead and are not offered a retry that cannot work. NotReadableError is untangled too: the camera exists and the permission is fine, and another app is holding it. A permission fixed in site settings is picked up live through the Permissions API change event rather than staying dead until a reload, and because MediaDevices is [SecureContext] the whole object is missing on http — where the obvious feature detect fires and produces the one wrong answer, "this browser has no camera support", so this component checks the context before it concludes anything. Fifth, mirroring. The preview is mirrored because a front camera shown unmirrored is disorienting, and the saved file is not, because a mirrored file makes every photographed document, badge, receipt and serial number read backwards; mirrorOutput is there for the selfie case and is off by default. Ships as a hook (useCameraCapture) plus a component, with videoProps to spread onto your own <video> so playsInline and muted — the two attributes an iPhone needs to keep the preview from going fullscreen over your page, and to be allowed to autoplay at all — cannot be forgotten. Audio is explicitly refused so the microphone indicator stays dark and only one permission is asked for. The camera list is read after a stream opens rather than on mount, because enumerateDevices answers before a grant with every label blank. Saved images can be capped with maxWidth and maxHeight, encode to JPEG by default, and report the type that actually came back rather than the one requested; the object URL is owned by the component and revoked on retake and unmount, so a capture screen used ten times does not pin ten images in memory. Controls are aria-disabled rather than disabled so a refusal stays reachable and can explain itself, with a permanently mounted polite live region for status and failures. Styled entirely with shadcn tokens (input, accent, ring, muted-foreground), so it follows light and dark mode, and its only dependency is lucide-react for the icons.

Live, loading

Starting the live preview…

Runs in a sandboxed frame on a separate origin, in its registry's own theme. Mounted with no props — an item that paints an empty box is given its name as text, and some items need input to show anything.

Notes from the author

Install any pulld component by name: add "@pulld": "https://pulld.pages.dev/r/{name}.json" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 95 components: https://pulld.pages.dev/?utm_source=cli
@pulld

More from @pulld