├── www ├── .gitignore ├── ui │ └── components │ │ ├── button │ │ ├── index.ts │ │ └── button.tsx │ │ ├── dialog │ │ ├── index.ts │ │ └── dialog.tsx │ │ └── index.ts ├── app │ ├── components │ │ ├── image-capture-dialog-desktop │ │ │ ├── index.ts │ │ │ └── image-capture-dialog-desktop.tsx │ │ ├── image-capture-dialog-mobile │ │ │ ├── index.ts │ │ │ └── image-capture-dialog-mobile.tsx │ │ └── index.ts │ ├── page.tsx │ ├── layout.tsx │ ├── content.tsx │ └── globals.css ├── public │ ├── og │ │ └── preview.png │ ├── icon-192x192.png │ ├── icon-512x512.png │ ├── manifest.json │ ├── github.svg │ ├── sw.js │ └── workbox-8ad7dfbc.js ├── postcss.config.mjs ├── lib │ └── utils.ts ├── next-env.d.ts ├── components.json ├── next.config.ts ├── tsconfig.json ├── package.json └── README.md ├── .npmrc ├── examples ├── with-vite │ ├── src │ │ ├── vite-env.d.ts │ │ ├── ui │ │ │ └── components │ │ │ │ ├── button │ │ │ │ ├── index.ts │ │ │ │ └── button.tsx │ │ │ │ ├── dialog │ │ │ │ ├── index.ts │ │ │ │ └── dialog.tsx │ │ │ │ └── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ └── views │ │ │ ├── ImageCaptureDialogDesktop.tsx │ │ │ └── ImageCaptureDialogMobile.tsx │ ├── tsconfig.json │ ├── .gitignore │ ├── vite.config.ts │ ├── index.html │ ├── components.json │ ├── tsconfig.node.json │ ├── eslint.config.js │ ├── tsconfig.app.json │ ├── package.json │ └── README.md └── with-nextjs │ ├── ui │ └── components │ │ ├── button │ │ ├── index.ts │ │ └── button.tsx │ │ ├── dialog │ │ ├── index.ts │ │ └── dialog.tsx │ │ └── index.ts │ ├── app │ ├── components │ │ ├── image-capture-dialog-mobile │ │ │ ├── index.ts │ │ │ └── image-capture-dialog-mobile.tsx │ │ ├── image-capture-dialog-desktop │ │ │ ├── index.ts │ │ │ └── image-capture-dialog-desktop.tsx │ │ └── index.ts │ ├── page.tsx │ ├── layout.tsx │ ├── content.tsx │ └── globals.css │ ├── postcss.config.mjs │ ├── next.config.ts │ ├── lib │ └── utils.ts │ ├── components.json │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── .npmignore ├── pnpm-workspace.yaml ├── src ├── index.ts └── WebCamera.tsx ├── tsconfig.json ├── .gitignore ├── vite.config.ts ├── LICENSE ├── package.json └── README.md /www/.gitignore: -------------------------------------------------------------------------------- 1 | out/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} -------------------------------------------------------------------------------- /www/ui/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button"; 2 | -------------------------------------------------------------------------------- /www/ui/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dialog"; 2 | -------------------------------------------------------------------------------- /examples/with-vite/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | *.log 4 | *.lock 5 | tsconfig.tsbuildinfo 6 | -------------------------------------------------------------------------------- /examples/with-nextjs/ui/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button"; 2 | -------------------------------------------------------------------------------- /examples/with-nextjs/ui/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dialog"; 2 | -------------------------------------------------------------------------------- /examples/with-vite/src/ui/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button"; 2 | -------------------------------------------------------------------------------- /examples/with-vite/src/ui/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dialog"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "." 3 | - "examples/*" 4 | - "www" 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./WebCamera"; 2 | 3 | export { WebCamera as default } from "./WebCamera"; 4 | -------------------------------------------------------------------------------- /www/app/components/image-capture-dialog-desktop/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./image-capture-dialog-desktop"; 2 | -------------------------------------------------------------------------------- /www/app/components/image-capture-dialog-mobile/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./image-capture-dialog-mobile"; 2 | -------------------------------------------------------------------------------- /www/public/og/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shivantra/react-web-camera/HEAD/www/public/og/preview.png -------------------------------------------------------------------------------- /www/public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shivantra/react-web-camera/HEAD/www/public/icon-192x192.png -------------------------------------------------------------------------------- /www/public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shivantra/react-web-camera/HEAD/www/public/icon-512x512.png -------------------------------------------------------------------------------- /examples/with-nextjs/app/components/image-capture-dialog-mobile/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./image-capture-dialog-mobile"; 2 | -------------------------------------------------------------------------------- /www/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/components/image-capture-dialog-desktop/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./image-capture-dialog-desktop"; 2 | -------------------------------------------------------------------------------- /www/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Content from "./content"; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/with-nextjs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Content from "./content"; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /www/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageCaptureDialogMobile } from "./image-capture-dialog-mobile"; 2 | export { ImageCaptureDialogDesktop } from "./image-capture-dialog-desktop"; 3 | -------------------------------------------------------------------------------- /examples/with-nextjs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageCaptureDialogMobile } from "./image-capture-dialog-mobile"; 2 | export { ImageCaptureDialogDesktop } from "./image-capture-dialog-desktop"; 3 | -------------------------------------------------------------------------------- /www/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /examples/with-nextjs/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /examples/with-vite/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /www/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /examples/with-vite/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/with-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /www/ui/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, buttonVariants } from "./button"; 2 | export { 3 | Dialog, 4 | DialogClose, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogOverlay, 10 | DialogPortal, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "./dialog"; 14 | -------------------------------------------------------------------------------- /examples/with-nextjs/ui/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, buttonVariants } from "./button"; 2 | export { 3 | Dialog, 4 | DialogClose, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogOverlay, 10 | DialogPortal, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "./dialog"; 14 | -------------------------------------------------------------------------------- /examples/with-vite/src/ui/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, buttonVariants } from "./button"; 2 | export { 3 | Dialog, 4 | DialogClose, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogOverlay, 10 | DialogPortal, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "./dialog"; 14 | -------------------------------------------------------------------------------- /examples/with-vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/with-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import path from "path"; 4 | import tailwindcss from "@tailwindcss/vite"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/with-vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "declarationDir": "dist", 9 | "emitDeclarationOnly": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | pnpm-debug.log* 6 | package-lock.json 7 | yarn.lock 8 | dist/ 9 | 10 | # Vite 11 | .vite/ 12 | .vitepress/ 13 | 14 | # Next 15 | .next/ 16 | 17 | # TypeScript 18 | *.tsbuildinfo 19 | 20 | # Testing 21 | coverage/ 22 | *.log 23 | *.lcov 24 | 25 | # Editor configs 26 | .vscode/ 27 | .idea/ 28 | *.swp 29 | 30 | # MacOS & Linux 31 | .DS_Store 32 | Thumbs.db 33 | 34 | # Misc 35 | .env 36 | .env.* 37 | playground/ -------------------------------------------------------------------------------- /www/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/ui/components", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-vite/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/ui/components", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-nextjs/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/ui/components", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "verbatimModuleSyntax": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /www/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import withPWAInit from "@ducanh2912/next-pwa"; 3 | 4 | const isProd = process.env.NODE_ENV === "production"; 5 | 6 | const withPWA = withPWAInit({ 7 | dest: "public", 8 | register: true, 9 | disable: !isProd, 10 | scope: "/react-web-camera/", 11 | }); 12 | 13 | const config: NextConfig = { 14 | output: "export", 15 | 16 | basePath: "/react-web-camera", 17 | 18 | assetPrefix: isProd ? "/react-web-camera/" : undefined, 19 | 20 | trailingSlash: true, 21 | 22 | images: { unoptimized: true }, 23 | }; 24 | 25 | export default withPWA(config); 26 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import dts from "vite-plugin-dts"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: "src/index.ts", 8 | name: "React Web Camera", 9 | fileName: (format) => 10 | format === "cjs" ? "react-web-camera.cjs" : "react-web-camera.js", 11 | formats: ["es", "cjs"], 12 | }, 13 | rollupOptions: { 14 | external: ["react", "react-dom"], 15 | output: { 16 | globals: { 17 | react: "React", 18 | "react-dom": "ReactDOM", 19 | }, 20 | }, 21 | }, 22 | }, 23 | plugins: [dts({ insertTypesEntry: true })], 24 | }); 25 | -------------------------------------------------------------------------------- /examples/with-nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/with-vite/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /examples/with-nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/with-vite/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /www/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Web Camera", 3 | "short_name": "React Web Camera", 4 | "description": "A lightweight React component to capture images from the browser camera. Switch front/back, control quality, and use it in PWAs.", 5 | "start_url": "/react-web-camera/?source=pwa", 6 | "scope": "/react-web-camera/", 7 | "display": "standalone", 8 | "background_color": "#ffffff", 9 | "theme_color": "#000000", 10 | "icons": [ 11 | { 12 | "src": "https://shivantra.com/react-web-camera/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "https://shivantra.com/react-web-camera/icon-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/with-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@radix-ui/react-dialog": "^1.1.15", 12 | "@radix-ui/react-slot": "^1.2.3", 13 | "class-variance-authority": "^0.7.1", 14 | "clsx": "^2.1.1", 15 | "lucide-react": "^0.541.0", 16 | "next": "15.5.0", 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0", 19 | "@shivantra/react-web-camera": "workspace:*", 20 | "tailwind-merge": "^3.3.1" 21 | }, 22 | "devDependencies": { 23 | "@tailwindcss/postcss": "^4", 24 | "@types/node": "^20", 25 | "@types/react": "^19", 26 | "@types/react-dom": "^19", 27 | "tailwindcss": "^4", 28 | "tw-animate-css": "^1.3.7", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /www/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "deploy": "npm run build && gh-pages --nojekyll -d out -b gh-pages" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.1.15", 13 | "@radix-ui/react-slot": "^1.2.3", 14 | "@shivantra/react-web-camera": "workspace:*", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.541.0", 18 | "next": "15.5.0", 19 | "react": "19.1.0", 20 | "react-dom": "19.1.0", 21 | "tailwind-merge": "^3.3.1" 22 | }, 23 | "devDependencies": { 24 | "@ducanh2912/next-pwa": "^10.2.9", 25 | "@tailwindcss/postcss": "^4", 26 | "@types/next-pwa": "^5.6.9", 27 | "@types/node": "^20", 28 | "@types/react": "^19", 29 | "@types/react-dom": "^19", 30 | "gh-pages": "^6.3.0", 31 | "tailwindcss": "^4", 32 | "tw-animate-css": "^1.3.7", 33 | "typescript": "^5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Shivantra Solutions Private Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/with-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-dialog": "^1.1.15", 14 | "@radix-ui/react-slot": "^1.2.3", 15 | "@tailwindcss/vite": "^4.1.12", 16 | "class-variance-authority": "^0.7.1", 17 | "clsx": "^2.1.1", 18 | "lucide-react": "^0.541.0", 19 | "react": "^19.1.1", 20 | "react-dom": "^19.1.1", 21 | "@shivantra/react-web-camera": "workspace:*", 22 | "tailwind-merge": "^3.3.1", 23 | "tailwindcss": "^4.1.12" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.33.0", 27 | "@types/node": "^24.1.0", 28 | "@types/react": "^19.1.10", 29 | "@types/react-dom": "^19.1.7", 30 | "@vitejs/plugin-react-swc": "^4.0.0", 31 | "eslint": "^9.33.0", 32 | "eslint-plugin-react-hooks": "^5.2.0", 33 | "eslint-plugin-react-refresh": "^0.4.20", 34 | "globals": "^16.3.0", 35 | "tw-animate-css": "^1.3.7", 36 | "typescript": "~5.8.3", 37 | "typescript-eslint": "^8.39.1", 38 | "vite": "^7.1.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /examples/with-nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shivantra/react-web-camera", 3 | "version": "1.0.0", 4 | "description": "Web camera component for React (by Shivantra)", 5 | "license": "MIT", 6 | "sideEffects": false, 7 | "type": "module", 8 | "main": "./dist/react-web-camera.cjs", 9 | "module": "./dist/react-web-camera.js", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/react-web-camera.js", 15 | "require": "./dist/react-web-camera.cjs" 16 | }, 17 | "./package.json": "./package.json" 18 | }, 19 | "files": [ 20 | "dist", 21 | "README.md", 22 | "LICENSE" 23 | ], 24 | "keywords": [ 25 | "react", 26 | "camera", 27 | "webcam", 28 | "media-devices", 29 | "components", 30 | "shivantra" 31 | ], 32 | "scripts": { 33 | "dev": "vite", 34 | "build": "vite build", 35 | "typecheck": "tsc --noEmit", 36 | "clean": "rimraf dist", 37 | "prepublishOnly": "npm run clean && npm run build" 38 | }, 39 | "peerDependencies": { 40 | "react": ">=17 || >=18", 41 | "react-dom": ">=17 || >=18" 42 | }, 43 | "devDependencies": { 44 | "@types/react": "^18.2.0", 45 | "@types/react-dom": "^18.2.0", 46 | "rimraf": "^6.0.0", 47 | "typescript": "^5.4.0", 48 | "vite": "^5.0.0", 49 | "vite-plugin-dts": "^3.7.0" 50 | }, 51 | "engines": { 52 | "node": ">=18.0.0" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/shivantra/react-web-camera.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/shivantra/react-web-camera/issues" 63 | }, 64 | "author": { 65 | "name": "Shivantra", 66 | "email": "contact@shivantra.com", 67 | "url": "https://shivantra.com" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /www/ui/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /examples/with-nextjs/ui/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /examples/with-vite/src/ui/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /examples/with-vite/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config([ 16 | globalIgnores(['dist']), 17 | { 18 | files: ['**/*.{ts,tsx}'], 19 | extends: [ 20 | // Other configs... 21 | 22 | // Remove tseslint.configs.recommended and replace with this 23 | ...tseslint.configs.recommendedTypeChecked, 24 | // Alternatively, use this for stricter rules 25 | ...tseslint.configs.strictTypeChecked, 26 | // Optionally, add this for stylistic rules 27 | ...tseslint.configs.stylisticTypeChecked, 28 | 29 | // Other configs... 30 | ], 31 | languageOptions: { 32 | parserOptions: { 33 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 34 | tsconfigRootDir: import.meta.dirname, 35 | }, 36 | // other options... 37 | }, 38 | }, 39 | ]) 40 | ``` 41 | 42 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 43 | 44 | ```js 45 | // eslint.config.js 46 | import reactX from 'eslint-plugin-react-x' 47 | import reactDom from 'eslint-plugin-react-dom' 48 | 49 | export default tseslint.config([ 50 | globalIgnores(['dist']), 51 | { 52 | files: ['**/*.{ts,tsx}'], 53 | extends: [ 54 | // Other configs... 55 | // Enable lint rules for React 56 | reactX.configs['recommended-typescript'], 57 | // Enable lint rules for React DOM 58 | reactDom.configs.recommended, 59 | ], 60 | languageOptions: { 61 | parserOptions: { 62 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 63 | tsconfigRootDir: import.meta.dirname, 64 | }, 65 | // other options... 66 | }, 67 | }, 68 | ]) 69 | ``` 70 | -------------------------------------------------------------------------------- /examples/with-vite/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import ImageCaptureDialogDesktop from "./views/ImageCaptureDialogDesktop"; 4 | import ImageCaptureDialogMobile from "./views/ImageCaptureDialogMobile"; 5 | import { Button } from "./ui/components"; 6 | import { Camera } from "lucide-react"; 7 | 8 | function useIsMobile() { 9 | const [isMobile, setIsMobile] = useState(window.innerWidth < 768); 10 | 11 | useEffect(() => { 12 | const handleResize = () => setIsMobile(window.innerWidth < 768); 13 | window.addEventListener("resize", handleResize); 14 | return () => window.removeEventListener("resize", handleResize); 15 | }, []); 16 | 17 | return isMobile; 18 | } 19 | 20 | function App() { 21 | const [open, setOpen] = useState(false); 22 | 23 | const isMobile = useIsMobile(); 24 | 25 | const handleOpen = () => setOpen(true); 26 | const handleClose = () => setOpen(false); 27 | 28 | return ( 29 |
30 | {/* Header */} 31 |
32 |
33 |
34 | 35 |
36 |

37 | React Web Camera 38 |

39 |

40 | Experience seamless image capture with our responsive camera 41 | component. Optimized for both desktop and mobile devices with 42 | intelligent UI adaptation. 43 |

44 |
45 | 46 | {/* Main Content */} 47 |
48 |
49 |
50 |
51 | 58 |

59 | {isMobile 60 | ? "Mobile-optimized interface" 61 | : "Desktop-enhanced experience"} 62 |

63 |
64 |
65 |
66 |
67 |
68 | 69 | {isMobile ? ( 70 | 71 | ) : ( 72 | 73 | )} 74 |
75 | ); 76 | } 77 | 78 | export default App; 79 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Camera } from "lucide-react"; 5 | 6 | import { Button } from "@/ui/components"; 7 | import { 8 | ImageCaptureDialogDesktop, 9 | ImageCaptureDialogMobile, 10 | } from "@/app/components"; 11 | 12 | function useIsMobile() { 13 | const [isMobile, setIsMobile] = useState(false); 14 | 15 | useEffect(() => { 16 | const checkMobile = () => setIsMobile(window.innerWidth < 768); 17 | 18 | checkMobile(); 19 | 20 | window.addEventListener("resize", checkMobile); 21 | 22 | return () => window.removeEventListener("resize", checkMobile); 23 | }, []); 24 | 25 | return isMobile; 26 | } 27 | 28 | function Content() { 29 | const [open, setOpen] = useState(false); 30 | 31 | const isMobile = useIsMobile(); 32 | 33 | const handleOpen = () => setOpen(true); 34 | const handleClose = () => setOpen(false); 35 | 36 | return ( 37 |
38 | {/* Header */} 39 |
40 |
41 |
42 | 43 |
44 |

45 | React Web Camera 46 |

47 |

48 | Experience seamless image capture with our responsive camera 49 | component. Optimized for both desktop and mobile devices with 50 | intelligent UI adaptation. 51 |

52 |
53 | 54 | {/* Main Content */} 55 |
56 |
57 |
58 |
59 | 66 |

67 | {isMobile 68 | ? "Mobile-optimized interface" 69 | : "Desktop-enhanced experience"} 70 |

71 |
72 |
73 |
74 |
75 |
76 | 77 | {isMobile ? ( 78 | 79 | ) : ( 80 | 81 | )} 82 |
83 | ); 84 | } 85 | 86 | export default Content; 87 | -------------------------------------------------------------------------------- /www/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import Script from "next/script"; 4 | import "./globals.css"; 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: "--font-geist-mono", 13 | subsets: ["latin"], 14 | }); 15 | 16 | const siteBase = "https://shivantra.com"; 17 | const basePath = "/react-web-camera"; 18 | const absolute = (p: string) => `${siteBase}${p}`; 19 | 20 | const isProd = process.env.NODE_ENV === "production"; 21 | const GA_ID = "G-N43VRYBMJ0"; 22 | 23 | export const metadata: Metadata = { 24 | metadataBase: new URL(siteBase), 25 | title: "React Web Camera – Shivantra", 26 | description: 27 | "A lightweight React component to capture images from the browser camera. Switch front/back, control quality, and use it in PWAs.", 28 | keywords: [ 29 | "react webcam", 30 | "react web camera", 31 | "react capture image", 32 | "react webcam component", 33 | "react camera front back", 34 | "pwa camera react", 35 | "react-web-camera", 36 | ], 37 | authors: [ 38 | { 39 | name: "Shivantra Solutions Private Limited", 40 | url: "https://shivantra.com", 41 | }, 42 | ], 43 | creator: "Shivantra", 44 | publisher: "Shivantra Solutions", 45 | alternates: { 46 | canonical: absolute(`${basePath}/`), 47 | }, 48 | openGraph: { 49 | title: "React Web Camera – Shivantra", 50 | description: 51 | "Capture images directly from the browser in React apps. Lightweight, mobile-friendly, and supports front/back switching.", 52 | url: absolute(`${basePath}/`), 53 | siteName: "Shivantra React Web Camera", 54 | images: [ 55 | { 56 | url: absolute(`${basePath}/og/preview.png`), 57 | width: 1200, 58 | height: 630, 59 | alt: "React Web Camera demo preview", 60 | }, 61 | ], 62 | locale: "en_US", 63 | type: "website", 64 | }, 65 | twitter: { 66 | card: "summary_large_image", 67 | title: "React Web Camera – Shivantra", 68 | description: 69 | "React component to access the browser camera, capture images, and switch between cameras.", 70 | images: [absolute(`${basePath}/og/preview.png`)], 71 | }, 72 | manifest: absolute(`${basePath}/manifest.json`), 73 | }; 74 | 75 | export default function RootLayout({ 76 | children, 77 | }: Readonly<{ 78 | children: React.ReactNode; 79 | }>) { 80 | return ( 81 | 82 | 85 | {children} 86 | {isProd && GA_ID && ( 87 | <> 88 | 102 | 103 | )} 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /examples/with-vite/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --radius-sm: calc(var(--radius) - 4px); 8 | --radius-md: calc(var(--radius) - 2px); 9 | --radius-lg: var(--radius); 10 | --radius-xl: calc(var(--radius) + 4px); 11 | --color-background: var(--background); 12 | --color-foreground: var(--foreground); 13 | --color-card: var(--card); 14 | --color-card-foreground: var(--card-foreground); 15 | --color-popover: var(--popover); 16 | --color-popover-foreground: var(--popover-foreground); 17 | --color-primary: var(--primary); 18 | --color-primary-foreground: var(--primary-foreground); 19 | --color-secondary: var(--secondary); 20 | --color-secondary-foreground: var(--secondary-foreground); 21 | --color-muted: var(--muted); 22 | --color-muted-foreground: var(--muted-foreground); 23 | --color-accent: var(--accent); 24 | --color-accent-foreground: var(--accent-foreground); 25 | --color-destructive: var(--destructive); 26 | --color-border: var(--border); 27 | --color-input: var(--input); 28 | --color-ring: var(--ring); 29 | --color-chart-1: var(--chart-1); 30 | --color-chart-2: var(--chart-2); 31 | --color-chart-3: var(--chart-3); 32 | --color-chart-4: var(--chart-4); 33 | --color-chart-5: var(--chart-5); 34 | --color-sidebar: var(--sidebar); 35 | --color-sidebar-foreground: var(--sidebar-foreground); 36 | --color-sidebar-primary: var(--sidebar-primary); 37 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 38 | --color-sidebar-accent: var(--sidebar-accent); 39 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 40 | --color-sidebar-border: var(--sidebar-border); 41 | --color-sidebar-ring: var(--sidebar-ring); 42 | } 43 | 44 | :root { 45 | --background: 0 0% 100%; 46 | --foreground: 222.2 84% 4.9%; 47 | 48 | --muted: 210 40% 96.1%; 49 | --muted-foreground: 215.4 16.3% 46.9%; 50 | 51 | --popover: 0 0% 100%; 52 | --popover-foreground: 222.2 84% 4.9%; 53 | 54 | --card: 0 0% 100%; 55 | --card-foreground: 222.2 84% 4.9%; 56 | 57 | --border: 214.3 31.8% 91.4%; 58 | --input: 214.3 31.8% 91.4%; 59 | 60 | --primary: 221.2 83.2% 53.3%; 61 | --primary-foreground: 210 40% 98%; 62 | 63 | --secondary: 210 40% 96.1%; 64 | --secondary-foreground: 222.2 47.4% 11.2%; 65 | 66 | --accent: 210 40% 96.1%; 67 | --accent-foreground: 222.2 47.4% 11.2%; 68 | 69 | --destructive: 0 72% 51%; 70 | --destructive-foreground: 210 40% 98%; 71 | 72 | --ring: 221.2 83.2% 53.3%; 73 | 74 | --radius: 0.5rem; 75 | } 76 | 77 | .dark { 78 | --background: 222.2 84% 4.9%; 79 | --foreground: 210 40% 98%; 80 | 81 | --muted: 217.2 32.6% 17.5%; 82 | --muted-foreground: 215 20.2% 65.1%; 83 | 84 | --popover: 222.2 84% 4.9%; 85 | --popover-foreground: 210 40% 98%; 86 | 87 | --card: 222.2 84% 4.9%; 88 | --card-foreground: 210 40% 98%; 89 | 90 | --border: 217.2 32.6% 17.5%; 91 | --input: 217.2 32.6% 17.5%; 92 | 93 | --primary: 217.2 91.2% 59.8%; 94 | --primary-foreground: 222.2 47.4% 11.2%; 95 | 96 | --secondary: 217.2 32.6% 17.5%; 97 | --secondary-foreground: 210 40% 98%; 98 | 99 | --accent: 217.2 32.6% 17.5%; 100 | --accent-foreground: 210 40% 98%; 101 | 102 | --destructive: 0 62.8% 30.6%; 103 | --destructive-foreground: 210 40% 98%; 104 | 105 | --ring: 224.3 76.3% 48%; 106 | 107 | --radius: 0.5rem; 108 | } 109 | 110 | @layer base { 111 | * { 112 | @apply border-border outline-ring/50; 113 | } 114 | body { 115 | @apply bg-background text-foreground; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /www/app/content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Camera } from "lucide-react"; 5 | import { Button } from "@/ui/components"; 6 | import { 7 | ImageCaptureDialogDesktop, 8 | ImageCaptureDialogMobile, 9 | } from "@/app/components"; 10 | 11 | function useIsMobile() { 12 | const [isMobile, setIsMobile] = useState(false); 13 | useEffect(() => { 14 | const checkMobile = () => setIsMobile(window.innerWidth < 768); 15 | checkMobile(); 16 | window.addEventListener("resize", checkMobile); 17 | return () => window.removeEventListener("resize", checkMobile); 18 | }, []); 19 | return isMobile; 20 | } 21 | 22 | function Content() { 23 | const [open, setOpen] = useState(false); 24 | const isMobile = useIsMobile(); 25 | 26 | const handleOpen = () => setOpen(true); 27 | const handleClose = () => setOpen(false); 28 | 29 | return ( 30 |
31 |
32 | 36 | Github logo 41 | 42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 |

50 | React Web Camera 51 |

52 |

53 | Default camera inputs only allow one photo, making multi-image 54 | capture frustrating.  55 | React web camera is a lightweight, headless React 56 | component for capturing multiple images in one session. It’s 57 | flexible, PWA-friendly, and gives you full control over UI and 58 | styling. 59 |

60 |
61 | 62 |
63 |
64 |
65 |
66 | 73 |

74 | {isMobile 75 | ? "Mobile-optimized interface" 76 | : "Desktop-enhanced experience"} 77 |

78 |
79 |
80 |
81 | 82 | 93 |
94 |
95 | 96 | {isMobile ? ( 97 | 98 | ) : ( 99 | 100 | )} 101 |
102 | ); 103 | } 104 | 105 | export default Content; 106 | -------------------------------------------------------------------------------- /examples/with-vite/src/ui/components/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { XIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Dialog({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function DialogTrigger({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return 17 | } 18 | 19 | function DialogPortal({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return 23 | } 24 | 25 | function DialogClose({ 26 | ...props 27 | }: React.ComponentProps) { 28 | return 29 | } 30 | 31 | function DialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function DialogContent({ 48 | className, 49 | children, 50 | showCloseButton = true, 51 | ...props 52 | }: React.ComponentProps & { 53 | showCloseButton?: boolean 54 | }) { 55 | return ( 56 | 57 | 58 | 66 | {children} 67 | {showCloseButton && ( 68 | 72 | 73 | Close 74 | 75 | )} 76 | 77 | 78 | ) 79 | } 80 | 81 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 82 | return ( 83 |
88 | ) 89 | } 90 | 91 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 92 | return ( 93 |
101 | ) 102 | } 103 | 104 | function DialogTitle({ 105 | className, 106 | ...props 107 | }: React.ComponentProps) { 108 | return ( 109 | 114 | ) 115 | } 116 | 117 | function DialogDescription({ 118 | className, 119 | ...props 120 | }: React.ComponentProps) { 121 | return ( 122 | 127 | ) 128 | } 129 | 130 | export { 131 | Dialog, 132 | DialogClose, 133 | DialogContent, 134 | DialogDescription, 135 | DialogFooter, 136 | DialogHeader, 137 | DialogOverlay, 138 | DialogPortal, 139 | DialogTitle, 140 | DialogTrigger, 141 | } 142 | -------------------------------------------------------------------------------- /www/ui/components/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | showCloseButton = true, 53 | ...props 54 | }: React.ComponentProps & { 55 | showCloseButton?: boolean 56 | }) { 57 | return ( 58 | 59 | 60 | 68 | {children} 69 | {showCloseButton && ( 70 | 74 | 75 | Close 76 | 77 | )} 78 | 79 | 80 | ) 81 | } 82 | 83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 84 | return ( 85 |
90 | ) 91 | } 92 | 93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 94 | return ( 95 |
103 | ) 104 | } 105 | 106 | function DialogTitle({ 107 | className, 108 | ...props 109 | }: React.ComponentProps) { 110 | return ( 111 | 116 | ) 117 | } 118 | 119 | function DialogDescription({ 120 | className, 121 | ...props 122 | }: React.ComponentProps) { 123 | return ( 124 | 129 | ) 130 | } 131 | 132 | export { 133 | Dialog, 134 | DialogClose, 135 | DialogContent, 136 | DialogDescription, 137 | DialogFooter, 138 | DialogHeader, 139 | DialogOverlay, 140 | DialogPortal, 141 | DialogTitle, 142 | DialogTrigger, 143 | } 144 | -------------------------------------------------------------------------------- /examples/with-nextjs/ui/components/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | showCloseButton = true, 53 | ...props 54 | }: React.ComponentProps & { 55 | showCloseButton?: boolean 56 | }) { 57 | return ( 58 | 59 | 60 | 68 | {children} 69 | {showCloseButton && ( 70 | 74 | 75 | Close 76 | 77 | )} 78 | 79 | 80 | ) 81 | } 82 | 83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 84 | return ( 85 |
90 | ) 91 | } 92 | 93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 94 | return ( 95 |
103 | ) 104 | } 105 | 106 | function DialogTitle({ 107 | className, 108 | ...props 109 | }: React.ComponentProps) { 110 | return ( 111 | 116 | ) 117 | } 118 | 119 | function DialogDescription({ 120 | className, 121 | ...props 122 | }: React.ComponentProps) { 123 | return ( 124 | 129 | ) 130 | } 131 | 132 | export { 133 | Dialog, 134 | DialogClose, 135 | DialogContent, 136 | DialogDescription, 137 | DialogFooter, 138 | DialogHeader, 139 | DialogOverlay, 140 | DialogPortal, 141 | DialogTitle, 142 | DialogTrigger, 143 | } 144 | -------------------------------------------------------------------------------- /www/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.145 0 0); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.145 0 0); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.145 0 0); 54 | --primary: oklch(0.205 0 0); 55 | --primary-foreground: oklch(0.985 0 0); 56 | --secondary: oklch(0.97 0 0); 57 | --secondary-foreground: oklch(0.205 0 0); 58 | --muted: oklch(0.97 0 0); 59 | --muted-foreground: oklch(0.556 0 0); 60 | --accent: oklch(0.97 0 0); 61 | --accent-foreground: oklch(0.205 0 0); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.922 0 0); 64 | --input: oklch(0.922 0 0); 65 | --ring: oklch(0.708 0 0); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0 0); 72 | --sidebar-foreground: oklch(0.145 0 0); 73 | --sidebar-primary: oklch(0.205 0 0); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.97 0 0); 76 | --sidebar-accent-foreground: oklch(0.205 0 0); 77 | --sidebar-border: oklch(0.922 0 0); 78 | --sidebar-ring: oklch(0.708 0 0); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.145 0 0); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.205 0 0); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.205 0 0); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.922 0 0); 89 | --primary-foreground: oklch(0.205 0 0); 90 | --secondary: oklch(0.269 0 0); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.269 0 0); 93 | --muted-foreground: oklch(0.708 0 0); 94 | --accent: oklch(0.269 0 0); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.556 0 0); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.205 0 0); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.269 0 0); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.556 0 0); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/with-nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.145 0 0); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.145 0 0); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.145 0 0); 54 | --primary: oklch(0.205 0 0); 55 | --primary-foreground: oklch(0.985 0 0); 56 | --secondary: oklch(0.97 0 0); 57 | --secondary-foreground: oklch(0.205 0 0); 58 | --muted: oklch(0.97 0 0); 59 | --muted-foreground: oklch(0.556 0 0); 60 | --accent: oklch(0.97 0 0); 61 | --accent-foreground: oklch(0.205 0 0); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.922 0 0); 64 | --input: oklch(0.922 0 0); 65 | --ring: oklch(0.708 0 0); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0 0); 72 | --sidebar-foreground: oklch(0.145 0 0); 73 | --sidebar-primary: oklch(0.205 0 0); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.97 0 0); 76 | --sidebar-accent-foreground: oklch(0.205 0 0); 77 | --sidebar-border: oklch(0.922 0 0); 78 | --sidebar-ring: oklch(0.708 0 0); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.145 0 0); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.205 0 0); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.205 0 0); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.922 0 0); 89 | --primary-foreground: oklch(0.205 0 0); 90 | --secondary: oklch(0.269 0 0); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.269 0 0); 93 | --muted-foreground: oklch(0.708 0 0); 94 | --accent: oklch(0.269 0 0); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.556 0 0); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.205 0 0); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.269 0 0); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.556 0 0); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/WebCamera.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | forwardRef, 3 | useCallback, 4 | useEffect, 5 | useImperativeHandle, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | 10 | export type CaptureType = "jpeg" | "png" | "webp"; 11 | export type CaptureQuality = 12 | | 0.1 13 | | 0.2 14 | | 0.3 15 | | 0.4 16 | | 0.5 17 | | 0.6 18 | | 0.7 19 | | 0.8 20 | | 0.9 21 | | 1; 22 | export type CaptureMode = "front" | "back"; 23 | export type FacingMode = "user" | "environment"; 24 | 25 | export interface WebCameraProps { 26 | className?: string; 27 | style?: React.CSSProperties; 28 | videoClassName?: string; 29 | videoStyle?: React.CSSProperties; 30 | getFileName?: () => string; 31 | captureMode?: CaptureMode; 32 | captureType?: CaptureType; 33 | captureQuality?: CaptureQuality; 34 | onError?: (err: Error) => void; 35 | } 36 | 37 | export type WebCameraHandler = { 38 | capture: () => Promise; 39 | switch: (facingMode?: FacingMode) => Promise; 40 | getMode: () => CaptureMode; 41 | }; 42 | 43 | const CAPTURE_MODES: Record = { 44 | back: "environment", 45 | front: "user", 46 | }; 47 | 48 | export const WebCamera = forwardRef( 49 | ( 50 | { 51 | className, 52 | style, 53 | videoClassName, 54 | videoStyle, 55 | getFileName, 56 | captureMode = "back", 57 | captureType = "jpeg", 58 | captureQuality = 0.8, 59 | onError, 60 | }, 61 | ref, 62 | ) => { 63 | const videoRef = useRef(null); 64 | const canvasRef = useRef(null); 65 | const [stream, setStream] = useState(null); 66 | const [facingMode, setFacingMode] = useState( 67 | CAPTURE_MODES[captureMode], 68 | ); 69 | const [devices, setDevices] = useState([]); 70 | 71 | const captureImage = useCallback(async () => { 72 | const video = videoRef.current; 73 | const canvas = canvasRef.current; 74 | 75 | if (!video || !canvas) return null; 76 | if (video.readyState < 2) return null; // not ready yet 77 | 78 | return new Promise((resolve) => { 79 | const context = canvas.getContext("2d")!; 80 | 81 | const width = video.videoWidth || 640; 82 | const height = video.videoHeight || 480; 83 | canvas.width = width; 84 | canvas.height = height; 85 | 86 | context.drawImage(video, 0, 0, width, height); 87 | 88 | const imageType = `image/${captureType}`; 89 | 90 | canvas.toBlob( 91 | async (blob) => { 92 | if (!blob) return; 93 | 94 | const file = new File( 95 | [blob], 96 | getFileName?.() ?? `capture-${Date.now()}.${captureType}`, 97 | { 98 | type: imageType, 99 | lastModified: Date.now(), 100 | }, 101 | ); 102 | 103 | resolve(file); 104 | }, 105 | imageType, 106 | captureQuality, 107 | ); 108 | }); 109 | }, [captureType, captureQuality, getFileName]); 110 | 111 | const switchCamera = useCallback(async () => { 112 | if (stream) { 113 | stream.getTracks().forEach((track) => track.stop()); 114 | if (videoRef.current) videoRef.current.srcObject = null; 115 | } 116 | 117 | const newFacingMode = facingMode === "user" ? "environment" : "user"; 118 | setFacingMode(newFacingMode); 119 | 120 | try { 121 | let constraints: MediaStreamConstraints; 122 | 123 | // fallback: if facingMode not supported, use deviceId 124 | if (devices.length >= 2) { 125 | const device = devices.find((d) => 126 | newFacingMode === "user" 127 | ? d.label.toLowerCase().includes("front") 128 | : d.label.toLowerCase().includes("back"), 129 | ); 130 | 131 | constraints = device 132 | ? { video: { deviceId: { exact: device.deviceId } } } 133 | : { video: { facingMode: { ideal: newFacingMode } } }; 134 | } else { 135 | constraints = { video: { facingMode: { ideal: newFacingMode } } }; 136 | } 137 | 138 | const newStream = 139 | await navigator.mediaDevices.getUserMedia(constraints); 140 | if (videoRef.current) videoRef.current.srcObject = newStream; 141 | setStream(newStream); 142 | } catch (err) { 143 | onError?.(err as Error); 144 | } 145 | }, [stream, facingMode, devices, onError]); 146 | 147 | useImperativeHandle( 148 | ref, 149 | () => ({ 150 | capture: captureImage, 151 | switch: switchCamera, 152 | getMode: () => (facingMode === "environment" ? "back" : "front"), 153 | }), 154 | [facingMode, captureImage, switchCamera], 155 | ); 156 | 157 | useEffect(() => { 158 | let mediaStream: MediaStream; 159 | 160 | const video = videoRef.current; 161 | 162 | const initCamera = async () => { 163 | try { 164 | // enumerate devices (helps iOS Safari + others) 165 | const allDevices = await navigator.mediaDevices.enumerateDevices(); 166 | setDevices(allDevices.filter((d) => d.kind === "videoinput")); 167 | 168 | mediaStream = await navigator.mediaDevices.getUserMedia({ 169 | video: { facingMode: { ideal: facingMode } }, 170 | }); 171 | 172 | if (video) { 173 | video.srcObject = mediaStream; 174 | video.onloadedmetadata = () => video?.play(); 175 | } 176 | setStream(mediaStream); 177 | } catch (err) { 178 | onError?.(err as Error); 179 | } 180 | }; 181 | 182 | initCamera(); 183 | 184 | return () => { 185 | mediaStream?.getTracks().forEach((track) => track.stop()); 186 | if (video) { 187 | video.srcObject = null; 188 | } 189 | }; 190 | }, [facingMode, onError]); 191 | 192 | return ( 193 |
194 | 210 | 211 |
212 | ); 213 | }, 214 | ); 215 | -------------------------------------------------------------------------------- /www/public/sw.js: -------------------------------------------------------------------------------- 1 | if(!self.define){let e,a={};const c=(c,s)=>(c=new URL(c+".js",s).href,a[c]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=c,e.onload=a,document.head.appendChild(e)}else e=c,importScripts(c),a()}).then(()=>{let e=a[c];if(!e)throw new Error(`Module ${c} didn’t register its module`);return e}));self.define=(s,t)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(a[n])return;let i={};const r=e=>c(e,n),f={module:{uri:n},exports:i,require:r};a[n]=Promise.all(s.map(e=>f[e]||r(e))).then(e=>(t(...e),i))}}define(["./workbox-8ad7dfbc"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/react-web-camera/_next/static/OAR5K7IylO0BhnVPTrBtG/_buildManifest.js",revision:"6cedb4275d21d9ae9dc83437430a1b5c"},{url:"/react-web-camera/_next/static/OAR5K7IylO0BhnVPTrBtG/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/react-web-camera/_next/static/chunks/520-796364b33448c7f9.js",revision:"796364b33448c7f9"},{url:"/react-web-camera/_next/static/chunks/981-4023fae46e128634.js",revision:"4023fae46e128634"},{url:"/react-web-camera/_next/static/chunks/app/_not-found/page-0867fb6c7e2d6b98.js",revision:"0867fb6c7e2d6b98"},{url:"/react-web-camera/_next/static/chunks/app/layout-f0081453ed287003.js",revision:"f0081453ed287003"},{url:"/react-web-camera/_next/static/chunks/app/page-bbde6a6eb6964ae2.js",revision:"bbde6a6eb6964ae2"},{url:"/react-web-camera/_next/static/chunks/dd098d2d-0f72804197df84b1.js",revision:"0f72804197df84b1"},{url:"/react-web-camera/_next/static/chunks/framework-849bdcb52d5d37d6.js",revision:"849bdcb52d5d37d6"},{url:"/react-web-camera/_next/static/chunks/main-7052f12a7df9ca79.js",revision:"7052f12a7df9ca79"},{url:"/react-web-camera/_next/static/chunks/main-app-d27f4b812b5e8a99.js",revision:"d27f4b812b5e8a99"},{url:"/react-web-camera/_next/static/chunks/pages/_app-a3ca044f39560f99.js",revision:"a3ca044f39560f99"},{url:"/react-web-camera/_next/static/chunks/pages/_error-33ded00fffe34d4a.js",revision:"33ded00fffe34d4a"},{url:"/react-web-camera/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/react-web-camera/_next/static/chunks/webpack-f37f3b65da78914b.js",revision:"f37f3b65da78914b"},{url:"/react-web-camera/_next/static/css/2d6b997afa552db4.css",revision:"2d6b997afa552db4"},{url:"/react-web-camera/_next/static/media/569ce4b8f30dc480-s.p.woff2",revision:"ef6cefb32024deac234e82f932a95cbd"},{url:"/react-web-camera/_next/static/media/747892c23ea88013-s.woff2",revision:"a0761690ccf4441ace5cec893b82d4ab"},{url:"/react-web-camera/_next/static/media/8d697b304b401681-s.woff2",revision:"cc728f6c0adb04da0dfcb0fc436a8ae5"},{url:"/react-web-camera/_next/static/media/93f479601ee12b01-s.p.woff2",revision:"da83d5f06d825c5ae65b7cca706cb312"},{url:"/react-web-camera/_next/static/media/9610d9e46709d722-s.woff2",revision:"7b7c0ef93df188a852344fc272fc096b"},{url:"/react-web-camera/_next/static/media/ba015fad6dcf6784-s.woff2",revision:"8ea4f719af3312a055caf09f34c89a77"},{url:"/react-web-camera/github.svg",revision:"83846e47ab54def74f675fca95a45064"},{url:"/react-web-camera/icon-192x192.png",revision:"f08af455a7aaad817ed9fa00136ff26f"},{url:"/react-web-camera/icon-512x512.png",revision:"fdb8e900f8ccf820f013e55897e819c2"},{url:"/react-web-camera/manifest.json",revision:"4c9e7dbd4da3961f335f307d56a5cb1a"},{url:"/react-web-camera/og/preview.png",revision:"b3e3e49656027ca231ca7c8581a73ffa"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/react-web-camera",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:function(e){var a=e.response;return _async_to_generator(function(){return _ts_generator(this,function(e){return[2,a&&"opaqueredirect"===a.type?new Response(a.body,{status:200,statusText:"OK",headers:a.headers}):a]})})()}}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(function(e){var a=e.sameOrigin,c=e.url.pathname;return!(!a||c.startsWith("/api/auth/callback")||!c.startsWith("/api/"))},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(function(e){var a=e.request,c=e.url.pathname,s=e.sameOrigin;return"1"===a.headers.get("RSC")&&"1"===a.headers.get("Next-Router-Prefetch")&&s&&!c.startsWith("/api/")},new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(function(e){var a=e.request,c=e.url.pathname,s=e.sameOrigin;return"1"===a.headers.get("RSC")&&s&&!c.startsWith("/api/")},new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(function(e){var a=e.url.pathname;return e.sameOrigin&&!a.startsWith("/api/")},new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(function(e){return!e.sameOrigin},new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Web Camera 2 | 3 | A lightweight and flexible React component for capturing images from the user’s camera (front or back) with support for `jpeg`, `png`, and `webp` formats. Built with modern React (`hooks` + `forwardRef`) and works on both desktop and mobile browsers. 4 | 5 | ## Table of Contents 6 | 7 | - [Why?](#why-) 8 | - [Our Solution](#our-solution-) 9 | - [Features](#features-) 10 | - [Installation](#installation-) 11 | - [Usage](#usage-) 12 | - [Basic Example](#basic-example) 13 | - [Vite.js Example](#vitejs-example) 14 | - [Next.js Example (App Router)](#nextjs-example-app-router) 15 | - [PWA Example](#pwa-example) 16 | - [Props](#props-%EF%B8%8F) 17 | - [Ref Methods](#ref-methods-) 18 | - [Notes](#notes-%EF%B8%8F) 19 | - [License](#license-) 20 | - [Contact](#contact-) 21 | 22 | --- 23 | 24 | ## Why? 25 | 26 | Capturing multiple images from a webcam is a common need in modern web apps, especially Progressive Web Apps (PWAs). 27 | Existing solutions are often: 28 | 29 | - Single-shot only (cannot capture multiple images) 30 | - Bloated or heavy 31 | - Hard to customize UI or styling 32 | - Not fully compatible with PWAs or mobile browsers 33 | 34 | **Problem with `` on mobile:** 35 | When you use a file input like: 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | on phones, it only allows **a single photo capture**. After you take one photo, the camera closes, and to capture another, the user must reopen the camera again. 42 | This creates a poor user experience for apps needing **multi-photo sessions** (for example: KYC verification, delivery apps, or documentation workflows). 43 | 44 | --- 45 | 46 | ## Our Solution 47 | 48 | `react-web-camera` provides a headless, platform-independent React component that gives you full control over your UI. It handles the complex logic of accessing the webcam, capturing multiple images, and managing state, while you focus on styling and user experience. 49 | 50 | This makes it: 51 | 52 | - **Lightweight** – minimal overhead for fast, responsive apps 53 | - **Flexible** – integrate seamlessly with your design system 54 | - **Multi-Image Ready** – capture and manage multiple photos in a single session 55 | 56 | --- 57 | 58 | ## Features 59 | 60 | - **📷 Front & Back Camera Support** – Easily capture images from both cameras. 61 | - **🖼 Multiple Image Formats** – Export images as jpeg, png, or webp. 62 | - **⚡ Adjustable Capture Quality** – Control image quality with a range of 0.1–1.0. 63 | - **🔄 Camera Switching** – Seamlessly switch between front (user) and back (environment) cameras. 64 | - **📸 Multi-Image Capture** – Click and manage multiple pictures within a session on both web and mobile. 65 | - **🎯 Camera Ready on Mount** – Access the camera instantly when the component loads. 66 | - **🛠 Full Programmatic Control** – Use ref methods like capture(), switch(), and getMode(). 67 | - **🎨 Custom Styling** – Style the container and video element to match your design system. 68 | 69 | --- 70 | 71 | ## Installation 72 | 73 | ```bash 74 | # If using npm 75 | npm install @shivantra/react-web-camera 76 | ``` 77 | 78 | ```bash 79 | # Or with yarn 80 | yarn add @shivantra/react-web-camera 81 | ``` 82 | 83 | ```bash 84 | # Or with pnpm 85 | pnpm add @shivantra/react-web-camera 86 | ``` 87 | 88 | --- 89 | 90 | ## Usage 91 | 92 | - **Basic Example** 93 | 94 | ```tsx 95 | import React, { useRef } from "react"; 96 | import { WebCamera, WebCameraHandler } from "@shivantra/react-web-camera"; 97 | 98 | function App() { 99 | const cameraHandler = useRef(null); 100 | const [images, setImages] = useState([]); 101 | 102 | async function handleCapture() { 103 | const file = await cameraHandler.current?.capture(); 104 | if (file) { 105 | const base64 = await fileToBase64(file); 106 | setImages((_images) => [..._images, base64]); 107 | } 108 | } 109 | 110 | function handleSwitch() { 111 | cameraHandler.current?.switch(); 112 | } 113 | 114 | return ( 115 |
116 |
117 | 118 | 119 |
120 |
121 | 127 |
128 |
129 | {images.map((image, ind) => ( 130 | 131 | ))} 132 |
133 |
134 | ); 135 | } 136 | ``` 137 | 138 | - **Vite.js Example** 139 | 140 | ```tsx 141 | import React from "react"; 142 | import ReactDOM from "react-dom/client"; 143 | import { WebCamera } from "@shivantra/react-web-camera"; 144 | 145 | function App() { 146 | return ( 147 |
148 |

📸 Vite + Webcam

149 | `vite-photo-${Date.now()}.jpeg`} 157 | /> 158 |
159 | ); 160 | } 161 | 162 | ReactDOM.createRoot(document.getElementById("root")!).render(); 163 | ``` 164 | 165 | - **Next.js Example (App Router)** 166 | 167 | ```tsx 168 | "use client"; 169 | 170 | import { WebCamera } from "@shivantra/react-web-camera"; 171 | 172 | export default function CameraPage() { 173 | return ( 174 |
175 |

📸 Next.js Webcam Example

176 | `next-photo-${Date.now()}.jpeg`} 183 | onError={(err) => console.error(err)} 184 | /> 185 |
186 | ); 187 | } 188 | ``` 189 | 190 | - **PWA Example** 191 | 192 | ```tsx 193 | import { WebCamera } from "@shivantra/react-web-camera"; 194 | 195 | export default function PWAApp() { 196 | return ( 197 |
198 |

📱 PWA Webcam Ready

199 | `pwa-photo-${Date.now()}.jpeg`} 208 | onError={(err) => console.error(err)} 209 | /> 210 |
211 | ); 212 | } 213 | ``` 214 | 215 | > ✅ Works on mobile browsers and when installed as a PWA (HTTPS required for camera access). 216 | 217 | --- 218 | 219 | ## Props ⚙️ 220 | 221 | | Prop | Type | Default | Description | 222 | | ---------------- | ------------------------------- | -------- | ------------------------------------------------ | 223 | | `className` | `string` | — | CSS class for the wrapper `
` | 224 | | `style` | `React.CSSProperties` | — | Inline styles for the wrapper `
` | 225 | | `videoClassName` | `string` | — | CSS class for the `