HTML IndexedDB

Beginner
⏱️ 18 min read
📚 Updated: Jul 2026
🎯 4 CRUD Tutorials + 3 Try It
indexedDB.open()

Introduction

IndexedDB is a low-level API for storing large amounts of structured data in the browser—including files and blobs. Unlike localStorage and sessionStorage, which only hold strings, IndexedDB gives you object stores, indexes, transactions, and key-based access. That makes it the right choice for offline apps, cached API responses, and complex client-side data.

This page is your central hub: learn core concepts here, browse 4 dedicated CRUD tutorials below, and practice with live Try It examples. If you are coming from Web SQL, think of object stores as tables—but you work with JavaScript objects, not SQL strings.

What You’ll Learn

01

open()

Create or open a DB.

02

Object stores

Save JS objects.

03

Transactions

Atomic read/write.

04

Cursors

Walk every record.

05

CRUD hub

4 deep tutorials.

06

Try It

Live browser demos.

What Is IndexedDB?

IndexedDB is a transactional database system built into modern browsers. You store and retrieve JavaScript objects using keys—similar to a NoSQL document store. The API is entirely asynchronous: requests return immediately and fire callbacks or resolve promises later, so the main thread never blocks while disk I/O runs.

IndexedDB is the W3C-standard replacement for deprecated Web SQL. It is supported in Chrome, Firefox, Safari, and Edge, making it the default choice for client-side structured storage in 2026.

💡
Beginner Tip

Always feature-detect with if (!window.indexedDB) before opening a database. Private browsing modes and very old browsers may not expose the API.

Key Features of IndexedDB

  • Structured data storage — save plain objects, arrays, Date values, ArrayBuffer, Blob, and File instances.
  • Indexing — create indexes on object properties for fast lookups without scanning every record.
  • Transactions — group multiple operations into one atomic unit; all succeed or all roll back.
  • Asynchronous API — event-driven requests (onsuccess, onerror) keep the UI responsive. Modern code can also use the promise-based indexedDB.databases() and wrapper libraries.
  • Origin-scoped persistence — data survives page reloads until the user clears site data or storage quota is exceeded.
  • Large capacity — browsers typically allow hundreds of megabytes per origin (varies by device and free disk space).

Basic Concepts

Before writing code, learn these terms—they appear in every IndexedDB program:

  • Database — the top-level container (opened with indexedDB.open(name, version)). One origin can have many databases.
  • Object store — a collection of records, like a table without fixed columns. Each record is a JavaScript object.
  • Key / keyPath — the unique identifier for a record. Set keyPath: 'id' on the store, or use autoIncrement: true to generate numeric keys.
  • Index — a secondary lookup path on a property (e.g. index on email for fast searches by email).
  • Transaction — a scope for reading or writing one or more object stores. Modes: readonly or readwrite.
  • Cursor — an iterator that walks records one at a time with cursor.continue().
  • Version upgrade — the only time you can create, rename, or delete object stores—inside onupgradeneeded when the version number increases.

Creating and Opening a Database

Call indexedDB.open('myDatabase', version). The second argument is a version number (integer). On first open, version 1 creates the database. To add a new object store later, bump the version and handle onupgradeneeded:

js
let db;

const request = indexedDB.open('myDatabase', 1);

request.onupgradeneeded = function (event) {
  db = event.target.result;
  const objectStore = db.createObjectStore('myObjectStore', { keyPath: 'id' });
  objectStore.createIndex('nameIndex', 'name', { unique: false });
};

request.onsuccess = function (event) {
  db = event.target.result;
  console.log('Database ready');
};

request.onerror = function (event) {
  console.error('Database error:', event.target.error.message);
};

onupgradeneeded runs before onsuccess when the version changes. Create all stores and indexes there—never inside a normal transaction on first visit.

Working with Object Stores

Object stores hold your records. To add data, start a readwrite transaction, get the store, and call add() or put():

js
function addItem(item) {
  const transaction = db.transaction(['myObjectStore'], 'readwrite');
  const objectStore = transaction.objectStore('myObjectStore');
  const request = objectStore.add(item);

  request.onsuccess = function () {
    console.log('Item added successfully.');
  };

  request.onerror = function (event) {
    console.error('Add item error:', event.target.error.message);
  };
}

addItem({ id: 1, name: 'Notebook', qty: 3 });

See the dedicated INSERT tutorial for add() vs put(), auto-increment keys, and storing blobs.

Transactions and Cursors

Transactions ensure operations complete as a unit. To read every record, open a readonly transaction and use openCursor():

js
function getAllItems() {
  const transaction = db.transaction(['myObjectStore'], 'readonly');
  const objectStore = transaction.objectStore('myObjectStore');
  const request = objectStore.openCursor();

  request.onsuccess = function (event) {
    const cursor = event.target.result;
    if (cursor) {
      console.log('Item:', cursor.value);
      cursor.continue();
    } else {
      console.log('No more items.');
    }
  };

  request.onerror = function (event) {
    console.error('Cursor error:', event.target.error.message);
  };
}

For a single key, store.get(key) is simpler. For many keys at once, getAll() returns an array. The Retrieve tutorial covers indexes and filtered cursors in depth.

Handling Errors

Every IndexedDB request can fail—quota exceeded, duplicate key on add(), or invalid transaction. Attach onerror handlers and read event.target.error.message (not the legacy errorCode property):

js
request.onerror = function (event) {
  console.error('Error:', event.target.error.message);
};

transaction.onabort = function () {
  console.warn('Transaction aborted — no changes were saved.');
};

transaction.oncomplete = function () {
  console.log('Transaction committed successfully.');
};

Also listen for db.onversionchange when another tab upgrades the database—close your connection so the upgrade can finish.

⚡ Quick Reference

TaskAPI pattern
Open databaseindexedDB.open(name, version)
Create store (upgrade only)db.createObjectStore(name, { keyPath, autoIncrement })
Insert new recordstore.add(object)
Insert or replacestore.put(object)
Read one keystore.get(key)
Iterate allstore.openCursor()
Delete onestore.delete(key)
Clear storestore.clear()
Open
indexedDB.open()

First step

Upgrade
onupgradeneeded

Schema only

Write
readwrite tx

add / put

Read
readonly tx

get / cursor

IndexedDB CRUD Tutorial Index

Search by operation or browse below. Each card links to a full tutorial with five examples, Try It editors, and FAQs.

CRUD Operations

4 tutorials

Focused tutorials for each database operation—with five examples, Try It editors, and FAQs on every page.

Examples Gallery

Five progressive examples from opening a database to a complete mini app. Use Try It Yourself to run live demos in your browser.

📚 Getting Started

Open a database and define your first object store.

Example 1 — Open Database & Create Store

Feature-detect IndexedDB, then open version 1 and create a contacts store in onupgradeneeded.

js
if (!window.indexedDB) {
  console.warn('IndexedDB not supported');
} else {
  const req = indexedDB.open('myDatabase', 1);
  req.onupgradeneeded = function (e) {
    e.target.result.createObjectStore('contacts', { keyPath: 'id', autoIncrement: true });
  };
  req.onsuccess = function () { console.log('Ready'); };
}
Try It Yourself

How It Works

indexedDB.open returns an IDBOpenDBRequest. Schema setup belongs in onupgradeneeded—it fires on first create and whenever you increase the version number.

Example 2 — Add an Item

Insert a contact inside a readwrite transaction. Use add() when the key must not already exist.

js
const tx = db.transaction(['contacts'], 'readwrite');
const store = tx.objectStore('contacts');
store.add({ name: 'Jane Doe', email: 'jane@example.com' });

tx.oncomplete = function () {
  console.log('Contact saved');
};
Try It Yourself

How It Works

With autoIncrement: true, the browser assigns the id key. The transaction commits when all requests succeed and oncomplete fires.

📈 Reading Data

Walk records with a cursor and speed up lookups with indexes.

Example 3 — List All Items with a Cursor

Cursors visit records one by one—useful for large stores when you should not load everything into memory at once.

js
const tx = db.transaction(['contacts'], 'readonly');
const req = tx.objectStore('contacts').openCursor();

req.onsuccess = function (e) {
  const cursor = e.target.result;
  if (cursor) {
    console.log(cursor.key, cursor.value.name);
    cursor.continue();
  }
};
Try It Yourself

How It Works

Call cursor.continue() to advance. When cursor is null, you have reached the end of the store.

Example 4 — Create an Index

Indexes let you query by email without scanning every contact. Create them during onupgradeneeded:

js
request.onupgradeneeded = function (event) {
  const store = event.target.result
    .createObjectStore('contacts', { keyPath: 'id', autoIncrement: true });
  store.createIndex('byEmail', 'email', { unique: true });
};

// Later: lookup by email
const index = tx.objectStore('contacts').index('byEmail');
index.get('jane@example.com').onsuccess = function (e) {
  console.log(e.target.result);
};
Try It Yourself

How It Works

Set unique: true to enforce one record per email. Index names are arbitrary strings you choose when creating the store.

🏗️ Complete Demo

A minimal HTML page with Add and Get All buttons—the pattern from the reference tutorial, improved.

Example 5 — Example Usage (Full Page)

This page opens a database, adds items on button click, and lists every record with a cursor:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>IndexedDB Example</title>
</head>
<body>
  <h1>IndexedDB Example</h1>
  <button id="add">Add Item</button>
  <button id="get">Get All Items</button>
  <script>
    let db;
    const request = indexedDB.open('myDatabase', 1);
    request.onupgradeneeded = function (e) {
      e.target.result.createObjectStore('myObjectStore', { keyPath: 'id', autoIncrement: true });
    };
    request.onsuccess = function (e) { db = e.target.result; };

    document.getElementById('add').addEventListener('click', function () {
      const tx = db.transaction(['myObjectStore'], 'readwrite');
      tx.objectStore('myObjectStore').add({ name: 'Sample Item ' + Date.now() });
    });

    document.getElementById('get').addEventListener('click', function () {
      const tx = db.transaction(['myObjectStore'], 'readonly');
      const req = tx.objectStore('myObjectStore').openCursor();
      req.onsuccess = function (e) {
        const cursor = e.target.result;
        if (cursor) {
          console.log('Item:', cursor.value);
          cursor.continue();
        }
      };
    });
  </script>
</body>
</html>
Try It Yourself

How It Works

Keep a module-level db reference after onsuccess. Button handlers start short transactions—never hold a transaction open while waiting for user input.

Use Cases

IndexedDB shines whenever the browser must own structured data without a live network:

  • Offline-first PWAs — cache API JSON, sync when back online.
  • Draft autosave — store form state locally so refresh does not lose work.
  • Media and file caching — persist Blob thumbnails or downloaded PDFs.
  • Game progress — save levels, inventory, and settings as objects.
  • Analytics buffers — queue events locally, flush in batches to the server.
  • Replacing Web SQL — migrate legacy SQL apps to object stores and indexes.

How IndexedDB Works (Step by Step)

  1. Detect support — confirm window.indexedDB exists.
  2. Open — call indexedDB.open(name, version).
  3. Upgrade schema — in onupgradeneeded, create object stores and indexes.
  4. Store connection — save db from onsuccess.
  5. Transact — for each operation, open readonly or readwrite transaction.
  6. Request — call add, put, get, delete, or openCursor on the object store.
  7. Handle result — use onsuccess / onerror; transaction oncomplete means data is persisted.

Best Practices

✅ Do

  • Create indexes for properties you query often
  • Group related writes in one readwrite transaction
  • Handle onerror and show user-friendly messages
  • Bump the version number for schema changes
  • Keep transactions short—no await user prompts inside
  • Use put() for updates, add() for strict inserts

❌ Don’t

  • Create object stores outside onupgradeneeded
  • Store huge blobs without cleanup or quota planning
  • Ignore QuotaExceededError on mobile devices
  • Assume localStorage can replace IndexedDB for objects
  • Open many overlapping readwrite transactions on the same store
  • Read errorCode—use error.message instead

📝 Notes

  • IndexedDB is not a SQL database—there is no SELECT * FROM syntax.
  • Data is isolated per origin (scheme + host + port). Other sites cannot read your database.
  • Users clearing browser data wipes IndexedDB for that site.
  • Private browsing may use in-memory storage that disappears when the session ends.
  • Continue to DELETE, INSERT, Retrieve, and UPDATE for operation-specific guides.
  • Coming from Web SQL? IndexedDB is the modern, cross-browser replacement.

Universal Browser Support

IndexedDB is supported in all major desktop and mobile browsers: Chrome, Firefox, Safari, and Edge. It is a W3C standard and the recommended API for client-side structured storage.

Baseline · Since HTML

IndexedDB API

IndexedDB is supported in all major desktop and mobile browsers: Chrome, Firefox, Safari, and Edge. It is a W3C standard and the recommended API for client-side structured storage.

96% Modern browser support
Google Chrome All versions · Desktop & Mobile
Full support
Mozilla Firefox All versions · Desktop & Mobile
Full support
Apple Safari All versions · macOS & iOS
Full support
Microsoft Edge All versions · Chromium & Legacy
Full support
Internet Explorer IE 6+ · Legacy environments
Full support
Opera All modern versions
Full support
IndexedDB API Excellent

Bottom line: Safe to use for new projects in 2026. Always feature-detect for defense in depth.

Conclusion

IndexedDB gives web applications a robust way to store structured data on the client. Open a database with indexedDB.open(), define object stores during onupgradeneeded, and perform every read or write inside a transaction. Cursors and indexes make large datasets manageable without blocking the UI.

You now have the conceptual foundation. Continue with the CRUD tutorials in the index above—DELETE, INSERT, Retrieve, and UPDATE—each with five examples and interactive Try It editors.

Key Takeaways

Knowledge Unlocked

Six things to remember about IndexedDB

Use these points when building offline-capable web apps.

6
Core concepts
🔄 02

Transactions

Every operation.

Required
📈 03

onupgradeneeded

Schema only.

Setup
🔍 04

Indexes

Fast lookups.

Query
05

Async

Non-blocking.

Performance
📚 06

CRUD hub

Go deeper.

Next

❓ Frequently Asked Questions

IndexedDB is a browser API for storing large amounts of structured data on the client. You open a database with indexedDB.open(), save JavaScript objects in object stores, and read or change them inside transactions. It works asynchronously so the page stays responsive.
localStorage stores simple key-value strings with a small size limit. IndexedDB stores objects, blobs, and files, supports indexes for fast lookups, and handles much larger datasets. Use localStorage for a few settings; use IndexedDB for offline apps and rich data.
The onupgradeneeded event runs when you open a database with a higher version number than before. Inside it you create or delete object stores and indexes. Normal add/get/delete operations happen later in readwrite or readonly transactions—not during upgrade.
add() inserts a new record and fails if the key already exists. put() inserts or replaces the record with the same key (upsert). Use add() when duplicates must be rejected; use put() for updates and idempotent saves.
IndexedDB groups work into transactions for consistency and concurrency. A readwrite transaction commits all writes together or rolls back on error. You request a transaction on one or more object store names, then call methods on transaction.objectStore('name').
Read the introduction and core concepts, try Example 1 in the gallery or the Try It editor, then open the CRUD tutorial index below. Start with DELETE or INSERT depending on whether you learn better by removing or adding data first.
Did you know?

IndexedDB runs in a separate browser thread from your JavaScript. That is why every operation is asynchronous—your main thread schedules work and receives results through events, keeping scroll and animations smooth even when reading thousands of records.

Practice IndexedDB basics

Open a database, create an object store, and add items in the Try It editor—all in your browser with no server required.

Open Try It editor →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

6 people found this page helpful