• 10 Mar, 2026

Suggested:

How to Build a Browser OCR Tool with Tesseract.js

How to Build a Browser OCR Tool with Tesseract.js

Learn how to build a browser-based OCR app using Tesseract.js. Extract text from images directly in the browser with a simple client-side setup.

Optical Character Recognition (OCR) allows you to extract text from images. With modern browser technologies and Tesseract.js, you can build a fully client-side OCR tool that runs directly in the browser without sending images to a server.  

In this tutorial, I’ll explain how I built a simple browser-based OCR application using Tesseract.js, HTML, CSS, and JavaScript. The project structure is lightweight and works entirely on the client side.  

What This Project Does  

This OCR web app allows users to:  

  • Upload an image
  • Process the image directly in the browser
  • Extract readable text using OCR  
  • Display the recognized text instantly  

     

Since the processing happens locally in the browser, it provides:   

  • Better privacy (images are not uploaded anywhere)
  • Faster processing
  • No server-side dependencies  

 

Project Folder Structure

ocr/
│
├── index.html
├── style.css
├── script.js
├── tesseract-assets/
│   ├── tesseract-core-simd.wasm.js
│   ├── tesseract-core.wasm.js
│   ├── tesseract.min.js
│   ├── worker.min.js
│   └── lang-data/
│       └── eng.traineddata

Each file and folder plays a specific role in making the OCR app work.

 

The Application Interface index.html  

This file contains the main structure of the OCR application.  

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image to Text · Text to Speech — Y2A OCR</title>
<!-- Google Fonts & modern UI icons -->
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-wrapper">
  <header class="header">
    <div class="logo-area">
      <h1>Y2A OCR & TTS</h1>
      <p>Image to text / multilingual speech</p>
    </div>
    <div class="badge">
      <i class="fas fa-microchip"></i> Tesseract.js + Web Speech
    </div>
  </header>
  <div class="main-grid">
    <div class="left-panel card">
      <div class="controls">
        <input type="file" id="fileInput" hidden accept="image/*">
        <button class="file-select-btn" onclick="document.getElementById('fileInput').click()">
          <i class="fas fa-image"></i> Select Image
        </button>
        <select id="langSelect" aria-label="Recognition language">
          <option value="eng">English</option>
        </select>
      </div>

      <div class="preview-area">
        <img id="imgPreview" src="#" alt="preview">
      </div>
      <div class="status-container">
        <div id="statusLabel"><i class="far fa-clock" style="margin-right: 6px;"></i>Ready, select image</div>
        <progress id="progressBar" value="0" max="1"></progress>
      </div>

      <button id="startBtn" disabled><i class="fas fa-magic" style="margin-right: 8px;"></i>Start Recognition</button>
    </div>
    <div style="display: flex; flex-direction: column; gap: 28px;">
      <div class="card right-panel" style="padding-bottom: 26px;">
        <div class="top-actions">
          <h3><i class="fas fa-align-left" style="color: var(--primary); margin-right: 8px;"></i>Extracted text</h3>
          <div class="action-btn-group">
            <button onclick="purifyText()" class="purple-btn"><i class="fas fa-broom"></i> Purify</button>
            <button onclick="copyText()"><i class="far fa-copy"></i> Copy</button>
            <button id="speakBtn" onclick="speakText()"><i class="fas fa-volume-up"></i> Speak</button>
            <button id="stopBtn" onclick="stopSpeech()" style="background:#ef4444; display: none;"><i class="fas fa-stop"></i> Stop</button>
            <div class="speak-speed">
              <label>⚡ <span id="speedVal">1</span>x</label>
              <input type="range" id="speedSlider" min="0.5" max="2" step="0.1" value="1">
            </div>
          </div>
        </div>
        <div id="output" contenteditable="true">⬆️ Upload an image, choose language & start</div>
      </div>
    </div>
  </div>

  <footer class="site-footer">
    <p>© 2026 — Free OCR & TTS tool</p>
    <div class="footer-links">
      <span><a href="https://y2asystem.com" style="font-weight:600; color:var(--primary); text-decoration: none;">Y2ASystem.com</a></span>
    </div>
  </footer>
</div>

<script src="./tesseract-assets/tesseract.min.js"></script>
<script src="script.js"></script>
</body>
</html>

 

Styling the UI –   style.css  

The stylesheet handles the visual design of the OCR application.  

 * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    :root {
      --primary: #2563eb;
      --primary-dark: #1d4ed8;
      --success: #10b981;
      --purple-accent: #8b5cf6;
      --bg: #f1f5f9;
      --surface: #ffffff;
      --text-primary: #0f172a;
      --text-secondary: #475569;
      --border-light: #e2e8f0;
      --shadow-sm: 0 4px 12px rgba(0,0,0,0.03);
      --shadow-md: 0 8px 20px rgba(0,0,0,0.05);
      --radius-card: 24px;
      --radius-element: 14px;
    }

    body {
      font-family: 'Inter', system-ui, -apple-system, sans-serif;
      background: var(--bg);
      color: var(--text-primary);
      line-height: 1.5;
      padding: 24px 20px;
    }

    .app-wrapper {
      max-width: 1440px;
      margin: 0 auto;
      display: flex;
      flex-direction: column;
      gap: 30px;
    }

    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: var(--surface);
      padding: 16px 28px;
      border-radius: 80px;
      box-shadow: var(--shadow-sm);
      border: 1px solid var(--border-light);
    }
    .logo-area h1 {
      font-size: 1.8rem;
      font-weight: 700;
      background: linear-gradient(135deg, #2563eb, #7c3aed);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      letter-spacing: -0.02em;
    }
    .logo-area p {
      font-size: 0.9rem;
      color: var(--text-secondary);
      font-weight: 500;
      margin-top: 2px;
    }
    .badge {
      background: #eef2ff;
      color: var(--primary-dark);
      padding: 8px 18px;
      border-radius: 40px;
      font-weight: 600;
      font-size: 0.9rem;
      display: flex;
      align-items: center;
      gap: 6px;
    }

    

    .main-grid {
      display: grid;
      grid-template-columns: 1fr 1.1fr;
      gap: 28px;
    }
    @media (max-width: 960px) {
      .main-grid {
        grid-template-columns: 1fr;
        gap: 24px;
      }
    }

    .card {
      background: var(--surface);
      border-radius: var(--radius-card);
      border: 1px solid var(--border-light);
      box-shadow: var(--shadow-sm);
      transition: all 0.25s ease;
      padding: 28px;
    }
    .card:hover {
      box-shadow: var(--shadow-md);
    }

    .left-panel .preview-area {
      margin-top: 20px;
      background: #fafcff;
      border-radius: 20px;
      border: 1px solid #e9edf2;
      overflow: hidden;
    }
    #imgPreview {
      width: 100%;
      max-height: 280px;
      object-fit: contain;
      display: none;
      background: #ffffff;
      transition: all 0.2s;
    }
    .controls {
      display: flex;
      gap: 12px;
      flex-wrap: wrap;
    }
    .file-select-btn {
      background: #334155;
      color: white;
      border: none;
      padding: 12px 22px;
      border-radius: 40px;
      font-weight: 600;
      font-size: 0.95rem;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
      transition: 0.15s;
    }
    .file-select-btn:hover {
      background: #1e293b;
    }
    select, button {
      border-radius: 40px;
      padding: 12px 22px;
      border: 1px solid #d1d9e6;
      background: white;
      font-family: 'Inter', sans-serif;
      font-weight: 500;
      font-size: 0.95rem;
      transition: all 0.15s;
      cursor: pointer;
    }
    select {
      background: #f8fafc;
      min-width: 170px;
      flex: 1;
    }
    select:hover {
      border-color: var(--primary);
    }
    button {
      background: var(--primary);
      color: white;
      border: none;
      font-weight: 600;
      padding: 12px 28px;
      box-shadow: 0 4px 8px rgba(37,99,235,0.2);
    }
    button:hover:not(:disabled) {
      background: var(--primary-dark);
      transform: translateY(-2px);
    }
    button:disabled {
      background: #b9c7da;
      box-shadow: none;
      cursor: not-allowed;
      opacity: 0.7;
    }
    #startBtn {
      width: 100%;
      margin-top: 22px;
      padding: 16px;
      border-radius: 40px;
      font-size: 1.1rem;
    }
    .status-container {
      margin-top: 24px;
      background: #f1f4f9;
      border-radius: 60px;
      padding: 10px 16px;
    }
    #statusLabel {
      font-size: 0.8rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: #465569;
      margin-bottom: 8px;
    }
    progress {
      width: 100%;
      height: 10px;
      border-radius: 20px;
      accent-color: var(--primary);
    }

    .right-panel .top-actions {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 22px;
      gap: 14px;
    }
    .top-actions h3 {
      font-size: 1.3rem;
      font-weight: 600;
    }
    .action-btn-group {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-items: center;
    }
    .action-btn-group button {
      padding: 8px 16px;
      font-size: 0.85rem;
      border-radius: 30px;
      background: #f1f5f9;
      color: #1e293b;
      box-shadow: none;
      font-weight: 500;
    }
    .action-btn-group button:hover {
      background: #e2e8f0;
      transform: none;
    }
    .purple-btn {
      background: var(--purple-accent) !important;
      color: white !important;
    }
    .purple-btn:hover {
      background: #7c3aed !important;
    }
    .speak-speed {
      display: flex;
      align-items: center;
      gap: 6px;
      background: #f1f5f9;
      padding: 4px 14px 4px 10px;
      border-radius: 40px;
      font-size: 0.8rem;
    }
    .speak-speed label {
      white-space: nowrap;
      color: #334155;
      font-weight: 500;
    }
    #speedSlider {
      width: 80px;
      accent-color: var(--primary);
    }
    #output {
      width: 100%;
      min-height: 280px;
      max-height: 360px;
      background: #f9fcff;
      border: 1px solid #dee7f0;
      border-radius: 28px;
      padding: 20px;
      font-size: 0.95rem;
      line-height: 1.6;
      overflow-y: auto;
      white-space: pre-wrap;
      font-family: 'SF Mono', 'Roboto Mono', monospace;
      box-shadow: inset 0 2px 6px rgba(0,0,0,0.02);
    }
    [contenteditable="true"]:focus {
      outline: 2px solid var(--primary);
      border-color: transparent;
    }

    .site-footer {
      background: var(--surface);
      border-radius: 60px;
      padding: 20px 30px;
      text-align: center;
      border: 1px solid var(--border-light);
      font-size: 0.9rem;
      color: #5f6c80;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
    }
    .footer-links a {
      text-decoration: none;
      color: var(--primary);
      font-weight: 500;
      margin: 0 6px;
    }

    @media (max-width: 600px) {
      .header {
        flex-direction: column;
        gap: 12px;
        border-radius: 40px;
      }
      .controls {
        flex-direction: column;
      }
    }

 

OCR Logic script.js     

This is where the core functionality lives.  

 (function() {
    const fileInput = document.getElementById('fileInput');
    const imgPreview = document.getElementById('imgPreview');
    const startBtn = document.getElementById('startBtn');
    const statusLabel = document.getElementById('statusLabel');
    const progressBar = document.getElementById('progressBar');
    const output = document.getElementById('output');
    const langSelect = document.getElementById('langSelect');
    const speedSlider = document.getElementById('speedSlider');
    const speedVal = document.getElementById('speedVal');
    const speakBtn = document.getElementById('speakBtn');
    const stopBtn = document.getElementById('stopBtn');

    window.onload = function() {
      if (typeof Tesseract !== 'undefined') {
        statusLabel.innerHTML = '<i class="far fa-check-circle" style="color: #10b981;"></i> Engine ready. Select image.';
      } else {
        statusLabel.innerText = '⚠️ Tesseract not loaded – check assets.';
        statusLabel.style.color = '#b91c1c';
      }
    };

    fileInput.onchange = function() {
      const file = fileInput.files[0];
      if (file) {
        const reader = new FileReader();
        reader.onload = (e) => {
          imgPreview.src = e.target.result;
          imgPreview.style.display = 'block';
          startBtn.disabled = false;
          statusLabel.innerHTML = '<i class="fas fa-image" style="color:#2563eb;"></i> Image loaded, press Start.';
        };
        reader.readAsDataURL(file);
      } else {
        imgPreview.style.display = 'none';
        startBtn.disabled = true;
      }
    };

    startBtn.onclick = async function() {
      const file = fileInput.files[0];
      const selectedLang = langSelect.value;
      if (!file) return;

      startBtn.disabled = true;
      output.innerText = '⏳ Recognizing text ...';

      try {
        const worker = await Tesseract.createWorker(selectedLang, 1, {
          workerPath: './tesseract-assets/worker.min.js',
          corePath: './tesseract-assets/tesseract-core-simd.wasm.js',
          langPath: './tesseract-assets/lang-data',
          gzip: false,
          logger: m => {
            if (m.status === 'recognizing text') {
              progressBar.value = m.progress || 0;
              statusLabel.innerHTML = `<i class="fas fa-spinner fa-pulse"></i> Recognizing ${Math.round(m.progress * 100)}%`;
            } else {
              statusLabel.innerText = m.status;
            }
          }
        });

        const { data: { text } } = await worker.recognize(file);
        output.innerText = text.trim() || '⚠️ No text detected.';
        statusLabel.innerHTML = '<i class="far fa-check-circle" style="color:#059669;"></i> Complete!';
        await worker.terminate();
      } catch (err) {
        console.error(err);
        output.innerText = '❌ OCR error. Make sure language data exists.';
        statusLabel.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#b91c1c;"></i> Recognition failed.';
      } finally {
        startBtn.disabled = false;
      }
    };

    speedSlider.oninput = function() {
      speedVal.innerText = speedSlider.value;
    };

    window.purifyText = function() {
      let text = output.innerText;
      if (!text || text.startsWith('Upload') || text.startsWith('⬆️')) return;
      const purified = text
        .replace(/[ \t]+/g, ' ')
        .replace(/([^\n])\n([^\n])/g, '$1 $2')
        .replace(/\n\s*\n/g, '\n\n')
        .trim();
      output.innerText = purified;
    };

    window.copyText = function() {
      navigator.clipboard.writeText(output.innerText);
      alert('Copied to clipboard!');
    };

    window.speakText = function() {
      const text = output.innerText;
      if (!text || text.startsWith('⬆️') || text.startsWith('Upload')) return;

      window.speechSynthesis.cancel();

      const utterance = new SpeechSynthesisUtterance(text);
      utterance.rate = parseFloat(speedSlider.value);

      const langMap = {
        'eng': 'en-US'
      };
      utterance.lang = langMap[langSelect.value] || 'en-US';

      speakBtn.style.display = 'none';
      stopBtn.style.display = 'inline-flex';

      utterance.onend = utterance.onerror = function() {
        speakBtn.style.display = 'inline-flex';
        stopBtn.style.display = 'none';
      };

      window.speechSynthesis.speak(utterance);
    };

    window.stopSpeech = function() {
      window.speechSynthesis.cancel();
      speakBtn.style.display = 'inline-flex';
      stopBtn.style.display = 'none';
    };
  })();

 

tesseract-assets – Local Tesseract Engine  

Instead of loading Tesseract from a CDN, this project stores all required files locally.  

This improves:  

  • Loading speed
  • Offline capability
  • Reliability in restricted environments  

 

Download Required Files

Before running the OCR application, you need to download the required Tesseract.js engine files and language data.

1. Tesseract.js Library

Download the main JavaScript library used to run OCR in the browser.

Download from the official repository:  
https://github.com/naptha/tesseract.js

You will need:

  • tesseract.min.js
  • worker.min.js

These files are usually found in the dist folder.

 

2. Tesseract WebAssembly Core

The OCR processing engine runs using WebAssembly.

Download the core files from:  
https://github.com/naptha/tesseract.js-core

Required files:

  • tesseract-core.wasm.js
  • tesseract-core-simd.wasm.js

These files handle the heavy OCR computation in the browser.

 

3. Language Data

Tesseract requires trained language data to recognize text.

Download the language files from the official repository:  
https://github.com/tesseract-ocr/tessdata

For this project, download:

  • eng.traineddata

Place it inside:

tesseract-assets/lang-data/

If you want to support more languages, simply download additional .traineddata files.

Live Demo  

If you're interested in expanding this project, you can test our live OCR demo and see how text extraction works directly in your browser.  

Ready to Preview?

Image to Text - OCR Tool

 

View Live Demo 

Conclusion  

Tesseract.js makes it surprisingly easy to add OCR capabilities directly inside the browser. By combining a simple HTML interface with local Tesseract assets, you can build a lightweight and privacy-friendly OCR tool.  

This kind of application is useful for:  

  • Digitizing documents
  • Extracting text from screenshots
  • Processing scanned forms
  • Internal automation tools  

And the best part is that it runs entirely on the client side.