🔋 IndexedDB
IndexedDB is a built-in browser database designed for storing large amounts of data on the client side.
Think of it as a powerful key–value storage system that can hold almost any JavaScript data type:
Numbers, strings, booleans
Objects, arrays
Dates, blobs, files, and more
❓ Why Use IndexedDB?
Enables your application to work offline by keeping data locally.
Ideal for storing large or complex datasets.
Automatically syncs with the server once the user is online again.
👉 Real-World Example
Google Docs uses IndexedDB to save your documents locally, letting you continue editing even without an internet connection.
Other Practical Use Cases
Note-taking apps
To-do list apps
Quiz or learning apps
Code editors
CMS or content-heavy tools
<div class="infographic-idbhierarchy">
<style>
.infographic-idbhierarchy {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--accent-2: #1d4ed8;
--accent-2-soft: #cfe0fb;
--outer: #eaf1fd;
--line: #ddd6c9;
--good: #1a8a5f;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-idbhierarchy[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--accent-2: #7ea6f7;
--accent-2-soft: #24365e;
--outer: #182338;
--line: #2c313d;
--good: #4fbf8f;
}
.infographic-idbhierarchy * { box-sizing: border-box; }
.infographic-idbhierarchy h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-idbhierarchy p { margin: 0; }
.infographic-idbhierarchy button { font-family: inherit; }
.ih-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.ih-icon svg { width: 100%; height: 100%; display: block; }
.ih-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ih-hero { text-align: center; margin-bottom: 1.4rem; }
.ih-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.ih-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- outer rectangle (IndexedDB / origin) --- */
.ih-origin { position: relative; max-width: 620px; margin: 0 auto 2.2rem; cursor: pointer; }
.ih-origin-body { position: relative; background: var(--outer); border: 2px solid var(--accent);
border-radius: 12px; padding: 1.6rem; transition: box-shadow 0.2s ease, border-color 0.2s ease; }
.ih-origin.is-selected .ih-origin-body { box-shadow: 0 0 0 3px var(--accent-soft); border-color: var(--accent-2); }
.ih-dbs { display: flex; gap: 1.4rem; justify-content: center; flex-wrap: wrap; margin-top: 1.6rem; }
/* --- database rectangles --- */
.ih-db-col { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; width: 210px; max-width: 44%; }
.ih-db { position: relative; width: 100%; cursor: pointer; }
.ih-db-body { position: relative; background: var(--accent-2-soft); border: 2px solid var(--accent-2);
border-radius: 10px; padding: 0.9rem 0.7rem; min-height: 6.5rem; transition: box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; }
.ih-db.is-selected .ih-db-body { box-shadow: 0 0 0 3px var(--accent-soft); border-color: var(--accent); background: var(--accent-soft); }
.ih-db-caption { font-size: 0.76rem; font-weight: 700; color: var(--ink); text-align: center; }
.ih-db-caption .ih-db-name { display: block; font-family: var(--mono-font); font-weight: 400; font-size: 0.66rem; color: var(--ink-soft); margin-top: 0.1rem; }
/* --- object store chips (paper stack) --- */
.ih-stores { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; align-items: flex-end; min-height: 2.6rem; }
.ih-store { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; cursor: pointer; }
.ih-store-paper { width: 30px; height: 34px; background: var(--paper); border: 1.5px solid var(--ink-soft); border-radius: 3px;
position: relative; box-shadow: 2px 2px 0 -1px var(--paper-alt), 2px 2px 0 0 var(--line); transition: border-color 0.2s ease, box-shadow 0.2s ease; }
.ih-store-paper::before, .ih-store-paper::after { content: ""; position: absolute; left: 5px; right: 5px; height: 2px; background: var(--line); }
.ih-store-paper::before { top: 9px; }
.ih-store-paper::after { top: 15px; }
.ih-store-label { font-family: var(--mono-font); font-size: 0.6rem; color: var(--ink-soft); }
.ih-store.is-selected .ih-store-paper { border-color: var(--accent); box-shadow: 2px 2px 0 -1px var(--accent-soft), 2px 2px 0 0 var(--accent); }
.ih-store.is-selected .ih-store-label { color: var(--accent); font-weight: 700; }
.ih-store-empty { font-family: var(--mono-font); font-size: 0.6rem; color: var(--ink-soft); opacity: 0.55; padding-bottom: 0.3rem; }
/* --- label plaque (IndexedDB chip, front-center) --- */
.ih-plaque { position: relative; z-index: 2; margin: -1.3rem auto 0; width: fit-content; background: var(--paper);
border: 2px solid var(--accent); border-radius: 6px; padding: 0.45rem 1.1rem; display: flex; align-items: center;
gap: 0.4rem; font-weight: 700; font-size: 0.8rem; color: var(--accent); cursor: pointer; transition: box-shadow 0.2s ease; }
.ih-plaque .ih-icon { width: 14px; height: 14px; }
.ih-origin.is-selected .ih-plaque { box-shadow: 0 0 0 3px var(--accent-soft); }
/* --- detail --- */
.ih-detail { max-width: 560px; margin: 1.2rem auto 0; text-align: center; background: var(--paper-alt);
border-left: 3px solid var(--accent); border-radius: 4px; padding: 0.85rem 1.1rem; min-height: 4.6rem;
display: flex; flex-direction: column; justify-content: center; gap: 0.3rem; }
.ih-detail-title { font-family: var(--display-font); font-size: 1rem; font-weight: 600; }
.ih-detail-desc { font-size: 0.8rem; color: var(--ink-soft); }
.ih-detail-code { font-family: var(--mono-font); font-size: 0.72rem; color: var(--accent); }
.ih-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.3rem; max-width: 560px; margin: 0 auto; }
.ih-note strong { color: var(--ink); }
@media (max-width: 560px) {
.ih-dbs { gap: 0.9rem; }
.ih-db-col { width: 150px; }
}
@media (prefers-reduced-motion: reduce) {
.infographic-idbhierarchy *, .infographic-idbhierarchy *::before, .infographic-idbhierarchy *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ih-wrap">
<div class="ih-hero">
<h1>IndexedDB Structure</h1>
<p>click the outer shell, a database, or an object store</p>
</div>
<div class="ih-origin" id="ihOrigin" data-part="indexeddb">
<div class="ih-origin-body">
<div class="ih-dbs">
<div class="ih-db-col">
<div class="ih-db" id="ihDbA" data-part="db-a">
<div class="ih-db-body">
<div class="ih-stores">
<div class="ih-store" data-part="store-users">
<div class="ih-store-paper" id="ihStoreUsersIcon"></div>
<span class="ih-store-label">users</span>
</div>
<div class="ih-store" data-part="store-products">
<div class="ih-store-paper" id="ihStoreProductsIcon"></div>
<span class="ih-store-label">products</span>
</div>
</div>
</div>
</div>
<div class="ih-db-caption">Database<span class="ih-db-name">"users_db"</span></div>
</div>
<div class="ih-db-col">
<div class="ih-db" id="ihDbB" data-part="db-b">
<div class="ih-db-body">
<div class="ih-stores">
<div class="ih-store-empty">(no stores opened yet)</div>
</div>
</div>
</div>
<div class="ih-db-caption">Database<span class="ih-db-name">"logs_db"</span></div>
</div>
</div>
</div>
<div class="ih-plaque" data-part="indexeddb">
<span class="ih-icon" id="ihPlaqueIcon"></span>IndexedDB
</div>
</div>
<div class="ih-detail" id="ihDetail">
<div class="ih-detail-title" id="ihDetailTitle">IndexedDB</div>
<div class="ih-detail-desc" id="ihDetailDesc">The browser's storage system, scoped to one origin. It can hold several databases, each fully isolated.</div>
<div class="ih-detail-code" id="ihDetailCode"></div>
</div>
<div class="ih-note">One origin → many <strong>databases</strong> → many <strong>object stores</strong> each — three separate levels of scope.</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-idbhierarchy')
|| document.querySelector('.infographic-idbhierarchy');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const ICONS = {
globe: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>'
};
wrapper.querySelector('#ihPlaqueIcon').innerHTML = ICONS.globe;
const detailTitle = wrapper.querySelector('#ihDetailTitle');
const detailDesc = wrapper.querySelector('#ihDetailDesc');
const detailCode = wrapper.querySelector('#ihDetailCode');
const allParts = wrapper.querySelectorAll('[data-part]');
const INFO = {
'indexeddb': {
title: 'IndexedDB',
desc: "The browser's storage system, scoped to one origin. It can hold several databases, each fully isolated.",
code: ''
},
'db-a': {
title: 'Database: "users_db"',
desc: 'One database, created by name + version. Everything inside belongs only to this database.',
code: 'indexedDB.open("users_db", 1)'
},
'db-b': {
title: 'Database: "logs_db"',
desc: 'A separate database in the same origin — same browser, same site, but nothing shared with users_db.',
code: 'indexedDB.open("logs_db", 1)'
},
'store-users': {
title: 'Object store: users',
desc: 'Like a table. Records are plain objects, looked up by a keyPath — here, probably "id".',
code: 'db.createObjectStore("users", { keyPath: "id" })'
},
'store-products': {
title: 'Object store: products',
desc: 'A second store in the same database — its own records, its own keyPath, its own transactions.',
code: 'db.createObjectStore("products", { keyPath: "id" })'
}
};
function select(part) {
allParts.forEach(el => el.classList.toggle('is-selected', el.dataset.part === part));
const info = INFO[part];
detailTitle.textContent = info.title;
detailDesc.textContent = info.desc;
detailCode.textContent = info.code;
}
allParts.forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
select(el.dataset.part);
});
});
select('indexeddb');
})();
</script>Top-level container.
One app usually has one database, but you can create more if needed.
A database contains object stores.
Similar to tables in SQL.
Stores records as key–value pairs.
Can also have indexes.
Help you search and query data quickly.
You create them on object stores.
Example: a "contacts" store may have indexes on email, first name, last name, etc.
♦️Basic IndexedDB concepts
The following briefly introduces the basic concepts in the IndexedDB :
IndexedDB databases store key-value pairs
Unlike localStorage and sessionStorage, the values stored in the IndexedDB can be complex structures like objects and blobs.
Keys can be the properties of these objects or binary objects.
For quick searching and sorting, you can create indexes using any property of the objects.
IndexedDB is transactional
Every read from and write to the IndexedDB databases happens in a transaction.
The transactional model ensures data integrity in case users open the web application in multiple tabs/windows and perform the read from and write to the same database.
IndexedDB API is mostly asynchronous
IndexedDB operations are asynchronous. They use DOM events to notify you when an operation completes and the result is available.
IndexedDB is a NoSQL system
The IndexedDB is a NoSQL system, meaning it uses queries that return a cursor, which then you can use to iterate through the result set.
IndexedDB follows the same-origin policy
An origin consists of the domain, protocol, and port of a URL where the code executes.
For example
https://developer.mozilla.org:domain : developer.mozilla.org
protocol: https
port: 443
The
https://developer.mozilla.org/en-US/docs****andhttps://www.javascripttutorial.net/are the same origin because they have the same domain, protocol, and port.However, the
http://developer.mozilla.organdhttps://developer.mozilla.orgaren’t the same origin since they have different protocols and ports :
|
| |
|---|---|---|
Protocol | https | http |
Port | 443 | 80 |
IndexedDB adheres to the same-origin policy, meaning each origin has its own set of databases. One origin cannot access databases from another origin.
Here is a clean, simplified rephrased version with clear code examples for each basic IndexedDB operation:
🪜 Basic IndexedDB Operations
These are the core actions you’ll perform when working with IndexedDB:
Open a connection to a database
Add (insert) data into an object store
Read data from an object store
Iterate through records using a cursor
Delete data from an object store
Below are simple examples for each step.
let db;
const request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = function (e) {
db = e.target.result;
// Create an object store only once (during upgrade)
if (!db.objectStoreNames.contains("users")) {
db.createObjectStore("users", { keyPath: "id" });
}
};
request.onsuccess = function (e) {
db = e.target.result;
console.log("Database opened successfully");
};
request.onerror = function () {
console.log("Failed to open database");
};function addUser(user) {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
store.add(user);
tx.oncomplete = () => console.log("User added");
tx.onerror = () => console.log("Error adding user");
}
addUser({ id: 1, name: "Alice", age: 25 });function getUser(id) {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const request = store.get(id);
request.onsuccess = () => {
console.log("User:", request.result);
};
}
getUser(1);function getAllUsers() {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
console.log("Record:", cursor.value);
cursor.continue(); // Move to the next record
} else {
console.log("No more records");
}
};
}
getAllUsers();function deleteUser(id) {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
store.delete(id);
tx.oncomplete = () => console.log("User deleted");
}
deleteUser(1);<div class="infographic-indexeddb">
<style>
.infographic-indexeddb {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--bad: #c23b3b;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-indexeddb[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--bad: #e06b6b;
}
.infographic-indexeddb * { box-sizing: border-box; }
.infographic-indexeddb h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-indexeddb p { margin: 0; }
.infographic-indexeddb button { font-family: inherit; }
.idb-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.idb-icon svg { width: 100%; height: 100%; display: block; }
.idb-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.idb-hero { text-align: center; margin-bottom: 1.3rem; }
.idb-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.idb-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- panels --- */
.idb-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.3rem; }
@media (max-width: 660px) { .idb-panels { grid-template-columns: 1fr; } }
.idb-panel { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--paper); }
.idb-panel-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); }
.idb-panel-title { display: flex; align-items: center; gap: 0.4rem; font-family: var(--mono-font); font-size: 0.72rem; font-weight: 700; color: var(--ink-soft); }
.idb-panel-title .idb-icon { width: 14px; height: 14px; color: var(--accent); }
.idb-panel-sub { font-family: var(--mono-font); font-size: 0.62rem; color: var(--ink-soft); opacity: 0.75; }
.idb-store-head { font-family: var(--mono-font); font-size: 0.72rem; color: var(--accent); font-weight: 700; padding: 0.5rem 0.75rem 0.2rem; }
.idb-store-sub { font-family: var(--mono-font); font-size: 0.62rem; color: var(--ink-soft); padding: 0 0.75rem 0.4rem; }
.idb-table-wrap { min-height: 8rem; max-height: 11rem; overflow-y: auto; padding: 0 0.6rem 0.5rem; }
.idb-table { width: 100%; border-collapse: collapse; font-family: var(--mono-font); font-size: 0.68rem; }
.idb-table th { text-align: left; color: var(--ink-soft); font-weight: 700; padding: 0.2rem 0.4rem; border-bottom: 1px solid var(--line); }
.idb-table td { padding: 0.3rem 0.4rem; color: var(--ink); }
.idb-table tr.is-row { animation: idb-pop-in 0.25s ease; border-radius: 4px; }
.idb-table tr.is-row:nth-child(odd) { background: var(--paper-alt); }
.idb-table tr.is-cursor td:first-child { color: var(--accent); font-weight: 700; }
.idb-table tr.is-cursor { background: var(--accent-soft) !important; }
.idb-table tr.is-leaving { animation: idb-pop-out 0.2s ease forwards; }
.idb-empty { font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); opacity: 0.7; padding: 0.5rem; }
@keyframes idb-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
@keyframes idb-pop-out { to { opacity: 0; transform: translateX(6px); } }
.idb-console { min-height: 8rem; max-height: 11rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.idb-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: idb-pop-in 0.2s ease; white-space: pre-wrap; }
.idb-console-line.is-echo { color: #7fb0ff; }
.idb-console-line.is-event { color: #d8a657; padding-left: 0.9rem; }
.idb-console-line.is-result { color: #9fd9a8; padding-left: 0.9rem; }
.idb-console-line.is-error { color: #e88; padding-left: 0.9rem; }
.idb-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
/* --- steps --- */
.idb-steps { display: flex; flex-direction: column; gap: 0.55rem; max-width: 580px; margin: 0 auto; }
.idb-step { display: flex; align-items: center; gap: 0.65rem; border: 1px solid var(--line); border-radius: 7px; padding: 0.5rem 0.65rem;
transition: opacity 0.25s ease; }
.idb-step.is-locked { opacity: 0.4; }
.idb-step-num { font-family: var(--display-font); font-size: 0.95rem; color: var(--accent); width: 1.2rem; flex-shrink: 0; text-align: center; }
.idb-step-icon { width: 26px; height: 26px; border-radius: 6px; background: var(--accent-soft); color: var(--accent);
display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.idb-step-icon .idb-icon { width: 15px; height: 15px; }
.idb-step-body { flex: 1; min-width: 0; }
.idb-step-title { font-size: 0.72rem; font-weight: 700; color: var(--ink); }
.idb-step-code { font-family: var(--mono-font); font-size: 0.64rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.idb-run-btn { flex-shrink: 0; background: var(--accent); color: #fff; border: none; border-radius: 5px; padding: 0.35rem 0.75rem;
font-size: 0.68rem; font-weight: 700; cursor: pointer; font-family: var(--mono-font); display: flex; align-items: center; gap: 0.3rem; }
.idb-run-btn:hover { opacity: 0.88; }
.idb-run-btn:disabled { background: var(--line); color: var(--ink-soft); cursor: not-allowed; opacity: 1; }
.idb-run-btn .idb-icon { width: 10px; height: 10px; }
.idb-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.3rem; max-width: 560px; margin: 0 auto; }
.idb-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-indexeddb *, .infographic-indexeddb *::before, .infographic-indexeddb *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="idb-wrap">
<div class="idb-panels">
<div class="idb-panel">
<div class="idb-panel-head">
<span class="idb-panel-title"><span class="idb-icon" id="idbStoreIcon"></span>users</span>
<span class="idb-panel-sub">object store</span>
</div>
<div class="idb-store-head" id="idbStoreHead">not opened yet</div>
<div class="idb-store-sub" id="idbStoreSub"></div>
<div class="idb-table-wrap" id="idbTableWrap">
<div class="idb-empty">run step 1 to open the database</div>
</div>
</div>
<div class="idb-panel">
<div class="idb-panel-head">
<span class="idb-panel-title" style="color:#d4d4d4">>_ Console</span>
</div>
<div class="idb-console" id="idbConsole">
<div class="idb-console-empty">// output appears here</div>
</div>
</div>
</div>
<div class="idb-steps" id="idbSteps">
<div class="idb-step" data-step="0">
<span class="idb-step-num">1️⃣</span>
<span class="idb-step-icon"><span class="idb-icon" id="idbIcon0"></span></span>
<span class="idb-step-body">
<div class="idb-step-title">Open the database</div>
<div class="idb-step-code">indexedDB.open("MyDatabase", 1)</div>
</span>
<button class="idb-run-btn" data-run="0" type="button">Run</button>
</div>
<div class="idb-step is-locked" data-step="1">
<span class="idb-step-num">2️⃣</span>
<span class="idb-step-icon"><span class="idb-icon" id="idbIcon1"></span></span>
<span class="idb-step-body">
<div class="idb-step-title">Add a record</div>
<div class="idb-step-code">store.add({ id, name, age })</div>
</span>
<button class="idb-run-btn" data-run="1" type="button" disabled>Run</button>
</div>
<div class="idb-step is-locked" data-step="2">
<span class="idb-step-num">3️⃣</span>
<span class="idb-step-icon"><span class="idb-icon" id="idbIcon2"></span></span>
<span class="idb-step-body">
<div class="idb-step-title">Read one record</div>
<div class="idb-step-code">store.get(1)</div>
</span>
<button class="idb-run-btn" data-run="2" type="button" disabled>Run</button>
</div>
<div class="idb-step is-locked" data-step="3">
<span class="idb-step-num">4️⃣</span>
<span class="idb-step-icon"><span class="idb-icon" id="idbIcon3"></span></span>
<span class="idb-step-body">
<div class="idb-step-title">Iterate with a cursor</div>
<div class="idb-step-code">store.openCursor() → cursor.continue()</div>
</span>
<button class="idb-run-btn" data-run="3" type="button" disabled>Run</button>
</div>
<div class="idb-step is-locked" data-step="4">
<span class="idb-step-num">5️⃣</span>
<span class="idb-step-icon"><span class="idb-icon" id="idbIcon4"></span></span>
<span class="idb-step-body">
<div class="idb-step-title">Delete a record</div>
<div class="idb-step-code">store.delete(1)</div>
</span>
<button class="idb-run-btn" data-run="4" type="button" disabled>Run</button>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-indexeddb')
|| document.querySelector('.infographic-indexeddb');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const ICONS = {
plug: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg>',
plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>',
search: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>',
scan: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/></svg>',
trash: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>',
database: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>'
};
wrapper.querySelector('#idbStoreIcon').innerHTML = ICONS.database;
wrapper.querySelector('#idbIcon0').innerHTML = ICONS.plug;
wrapper.querySelector('#idbIcon1').innerHTML = ICONS.plus;
wrapper.querySelector('#idbIcon2').innerHTML = ICONS.search;
wrapper.querySelector('#idbIcon3').innerHTML = ICONS.scan;
wrapper.querySelector('#idbIcon4').innerHTML = ICONS.trash;
const storeHead = wrapper.querySelector('#idbStoreHead');
const storeSub = wrapper.querySelector('#idbStoreSub');
const tableWrap = wrapper.querySelector('#idbTableWrap');
const consoleEl = wrapper.querySelector('#idbConsole');
const steps = wrapper.querySelectorAll('.idb-step');
const runBtns = wrapper.querySelectorAll('[data-run]');
let dbOpen = false;
let records = [];
const POOL = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 31 },
{ id: 3, name: 'Carol', age: 28 }
];
let addPointer = 0;
function logLine(text, cls) {
if (consoleEl.querySelector('.idb-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'idb-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function renderTable() {
if (!dbOpen) {
storeHead.textContent = 'not opened yet';
storeSub.textContent = '';
tableWrap.innerHTML = '<div class="idb-empty">run step 1 to open the database</div>';
return;
}
storeHead.textContent = 'users {count: ' + records.length + '}';
storeSub.textContent = "keyPath: 'id'";
if (records.length === 0) {
tableWrap.innerHTML = '<div class="idb-empty">empty \u2014 add a record</div>';
return;
}
const table = document.createElement('table');
table.className = 'idb-table';
table.innerHTML = '<thead><tr><th>id</th><th>name</th><th>age</th></tr></thead>';
const tbody = document.createElement('tbody');
records.forEach(r => {
const tr = document.createElement('tr');
tr.className = 'is-row';
tr.dataset.id = r.id;
tr.innerHTML = '<td>' + r.id + '</td><td>' + r.name + '</td><td>' + r.age + '</td>';
tbody.appendChild(tr);
});
table.appendChild(tbody);
tableWrap.innerHTML = '';
tableWrap.appendChild(table);
}
function unlockSteps() {
steps.forEach(s => s.classList.remove('is-locked'));
runBtns.forEach(b => { if (b.dataset.run !== '0') b.disabled = false; });
}
const RUNS = [
() => {
logLine('> const request = indexedDB.open("MyDatabase", 1);', 'is-echo');
logLine('onupgradeneeded \u25b8 creating object store "users" (keyPath: "id")', 'is-event');
logLine('onsuccess \u25b8 Database opened successfully', 'is-event');
dbOpen = true;
unlockSteps();
renderTable();
},
() => {
if (!dbOpen) return;
const user = POOL[addPointer % POOL.length];
addPointer += 1;
logLine('> addUser({ id: ' + user.id + ', name: "' + user.name + '", age: ' + user.age + ' });', 'is-echo');
const exists = records.some(r => r.id === user.id);
if (exists) {
logLine('onerror \u25b8 ConstraintError: key ' + user.id + ' already exists in "users"', 'is-error');
return;
}
records.push(user);
renderTable();
logLine('oncomplete \u25b8 User added', 'is-result');
},
() => {
if (!dbOpen) return;
logLine('> getUser(1);', 'is-echo');
const rec = records.find(r => r.id === 1);
if (rec) {
logLine('onsuccess \u25b8 User: {id: ' + rec.id + ', name: "' + rec.name + '", age: ' + rec.age + '}', 'is-result');
} else {
logLine('onsuccess \u25b8 User: undefined', 'is-event');
}
},
() => {
if (!dbOpen) return;
logLine('> getAllUsers();', 'is-echo');
if (records.length === 0) {
logLine('onsuccess \u25b8 No more records', 'is-result');
return;
}
let i = 0;
const rows = tableWrap.querySelectorAll('tr.is-row');
function step() {
rows.forEach(r => r.classList.remove('is-cursor'));
if (i >= records.length) {
logLine('onsuccess \u25b8 No more records', 'is-result');
return;
}
const rec = records[i];
if (rows[i]) rows[i].classList.add('is-cursor');
logLine('onsuccess \u25b8 Record: {id: ' + rec.id + ', name: "' + rec.name + '", age: ' + rec.age + '}', 'is-event');
i += 1;
setTimeout(step, 420);
}
step();
},
() => {
if (!dbOpen) return;
logLine('> deleteUser(1);', 'is-echo');
const row = tableWrap.querySelector('tr[data-id="1"]');
const existed = records.some(r => r.id === 1);
records = records.filter(r => r.id !== 1);
if (row) {
row.classList.add('is-leaving');
setTimeout(renderTable, 180);
} else {
renderTable();
}
logLine('oncomplete \u25b8 User deleted' + (existed ? '' : ' (no matching key \u2014 still succeeds)'), 'is-result');
}
];
runBtns.forEach(btn => {
btn.addEventListener('click', () => {
RUNS[Number(btn.dataset.run)]();
});
});
renderTable();
})();
</script>