Telerik blogs

In this article, we will explore how to use OPFS to store files directly on the user’s device and why it is great for privacy and good offline experiences on websites.

In the early days of the web, cookies were the only way for websites to store data on a user’s device. They were mainly designed for server-side usage, capped at 4 KB per cookie, and were automatically sent with every HTTP request.

With the increase in storage requirements and websites becoming more interactive, the Web Storage API was created. The Web Storage API, via the localStorage and sessionStorage objects, allowed websites to store more data locally on users’ browsers (up to 5MB). But unlike cookies, the data is not included in every HTTP request, which means stored data is more secure and the website will have better performance. Even though the Web Storage API is great, it can only store strings, lacks a structured file access system and isn’t built for large data, especially with the recent data explosion.

Then came IndexedDB, which lets web applications store large amounts of structured data (100+ MB), like objects, arrays and even files directly on the user’s device using a key-value system database. It’s still widely used, but it’s not beginner-friendly and its API is lengthy and repetitive, and storing actual files isn’t straightforward.

To address these issues, we now have Origin Private File System (OPFS), a straightforward file-based system exposed through an API that gives web applications access to a structured file access system directly on the user’s device. It allows writing and reading of large files (300+ MB) of all kinds and even folders without ever needing to send them to a server.

In this article, we will explore how to use OPFS to store files directly on the user’s device and why it’s great for privacy and good offline experiences on websites.

What Is OPFS?

OPFS is a special type of storage that lives inside the browser and is part of the File System Access API. What makes OPFS unique is that it’s private to your website’s origin, kind of like a hidden folder that only your site can see and use.

Unlike the regular file system on our computers, OPFS is virtual. The user doesn’t see or interact with the files directly.

OPFS works only in secure contexts (i.e., over HTTPS) and when testing on localhost. Stored data persists across browser restarts, just like saving a file to a hard drive.

You also don’t need to ask for permission to use OPFS, because there is no file picker or popup involved. Your app can read from and write to OPFS freely in the background. Just like other browser storage APIs, OPFS is subject to browser storage quota restrictions. The limit is not a fixed number, but depending on the device and browser, it usually falls between 300 MB to several gigabytes. The browser will dynamically allocate storage quota based on the device storage capacity, used space, site visit frequency, etc. You can check storage space being used by OPFS via navigator.storage.estimate().

Benefits of OPFS

  • Invisible to the user – Acts like a virtual folder that only your app can access
  • No permissions needed – You don’t need to ask the user for access
  • Origin-scoped – Only your website (on your domain) can see its files
  • Persistent – Data stays even after the browser is closed or restarted
  • Structured like a real file system – You can create folders, read/write files and store both text and binary data

Use Cases of OPFS

  • Building offline-first applications that work fine offline and sync later when online
  • Browser games that need to store large game state files for offline play
  • Personal document management like note-taking apps
  • Optimizing web-based video/audio editing apps
  • Optimizing web-based file editors

How to Access the OPFS

To access OPFS, we must get access to the root directory. To do that, we need to call navigator.storage.getDirectory(), which is a function that returns an object called FileSystemDirectoryHandle. This object represents the root directory of your OPFS.

The FileSystemDirectoryHandle Object

While the FileSystemDirectoryHandle object represents the root directory within the OPFS in this context, its meaning is not exclusive to just OPFS. The FileSystemDirectoryHandle object is a part of the broader File System Access API, and it can represent any directory depending on how you obtain it.

The two main ways to a FileSystemDirectoryHandle object include:

  • Via navigator.storage.getDirectory() – where it would now represent the root of your OPFS
  • Via File picker window.showDirectoryPicker() – where it would now represent a real directory on the user’s local device

Every other way we may obtain a FileSystemDirectoryHandle would represent a subdirectory (child or root) of either of the two main ways. In the context where the FileSystemDirectoryHandle object represents the root of our OPFS, the FileSystemDirectoryHandle object exposes methods that allow us to perform operations such as:

  • Creating or accessing files: getFileHandle(fileName, options)
  • Creating or accessing subdirectories: getDirectoryHandle()
  • Removing files or directories: removeEntry()
  • Listing contents: for await (const entry of dirHandle.values())

Prerequisites

To follow along with this tutorial, you should be familiar with basic HTML, CSS and JavaScript.

Browser Support of OPFS

While the Origin Private File System may be supported by most modern browsers, it works mainly in Chromium-based browsers (like Chrome and Edge).

What We Are Building

In this post, we are going to build a simple notepad application that stores text note files (.txt) in the OPFS and persists them even after page refreshes or when the browser closes.

The notepad app will have an input for the title, which will become the filename, a body for the note itself, a “Save Note” button and a “Refresh notes” button.

All notes saved to the OPFS will also be listed so we can see what we save. Each saved note will also have a “Load Note” button that reads the note back from the OPFS and a “Delete note” button that deletes the file from the OPFS.

Project Setup

  • First, create a folder where this app will live. I will call my folder notesApp.
  • Then launch your code editor and open that created folder.
  • Create an empty file called app.html.
  • Open app.html in a secure domain context or test with localhost as we will be doing in this guide using the VS Code Live Server.

Building a Basic User Interface

Let’s start by building a user Interface. Add the following to your app.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini Notepad (OPFS)</title>
    <style>
      body {
        font-family: sans-serif;
        padding: 2rem;
        max-width: 600px;
        margin: auto;
      }
      input,
      textarea {
        width: 100%;
        margin-bottom: 1rem;
        padding: 0.5rem;
      }
      button {
        margin-right: 0.5rem;
        margin-top: 0.5rem;
        padding: 0.5rem 1rem;
      }
      .status {
        margin-top: 1rem;
        color: green;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      li {
        margin-bottom: 0.5rem;
      }
    </style>
  </head>
  <body>
    <h1>Mini Notepad (OPFS)</h1>
    <input type="text" id="title" placeholder="Enter note title..." />
    <textarea id="note" placeholder="Compose your note here..."></textarea>
    <button>Save Note</button>
    <button>Refresh Notes List</button>
    <section>
      <h2>Saved Notes</h2>
      <ul id="notes-list"></ul>
    </section>
    <div class="status" id="status"></div>
    <script>
      const notesListEl = document.getElementById("notes-list");
      const statusEl = document.getElementById("status");
      const titleEl = document.getElementById("title");
      const noteEl = document.getElementById("note");

      function showStatus(msg) {
        statusEl.textContent = msg;
      }
    </script>
  </body>
</html>

We have created a basic HTML and CSS boilerplate webpage. We also have a saved notes section that will display all note files saved to OPFS and a status div element that will hold all success or error messages so we do not have to use the console.

In our script, we have referenced by ID all the elements of our app that may be dynamically updated or copied via our script. And lastly, we created a showStatus function that updates our status element with whatever message we output.

Now launch app.html with VS Code Live Server and you should have an interface that looks like this:

The user interface

Accessing the OPFS Root Directory

Add the following to your app.html file:

async function getOPFSRoot() {
  if (!("storage" in navigator && "getDirectory" in navigator.storage)) {
    alert("OPFS is not supported in this browser.");
    throw new Error("OPFS not supported");
  }
  return await navigator.storage.getDirectory();
}

To access the OPFS root directory, we created an asynchronous function called getOPFSRoot().

The function first checks if OPFS is supported in the browser you are using and then calls navigator.storage.getDirectory(), which obtains OPFS’s FileSystemDirectoryHandle and returns it.

By doing this, whenever we call getOPFSRoot() anywhere in app.html, we have access to the OPFS root directory.

How to Store a File in OPFS

Add the following to your app.html file:

async function saveNote() {
  const title = titleEl.value.trim();
  const content = noteEl.value;
  if (!title) return showStatus("Please enter a title.");
  try {
    const root = await getOPFSRoot();
    const handle = await root.getFileHandle(`${title}.txt`, { create: true });
    const writable = await handle.createWritable();
    await writable.write(content);
    await writable.close();
    showStatus(`Note '${title}' saved.`);
    loadNotesList();
  } catch (err) {
    console.error(err);
    showStatus("Error saving note.");
  }
}

To store a note file, we created a function called saveNote() that contains the logic so we can call the saveNote() function whenever we want. The function will first clean up the title input value because the title we provide will become the name of the text file we will save.

Our function will return an error status if the title input is empty because every file is required to have a name in order to be saved. Then we call our getOPFSRoot() function, which, through FileSystemDirectoryHandle, gives us access to the getFileHandle(fileName, options) function.

The getFileHandle() may get a handle to an existing file or, as in this case create: true, create a new file called {title}.txt.

Next, we open the {title}.txt file with handle.createWritable(), while still using the same handle.

This is similar to opening a regular file on your PC when you want to edit it. We then write the content of our note into the file with await writable.write(content). Here, content is a reference to the value of the <textarea> element, which is our note body.

Lastly, we call await writable.close(), which closes the file and consequently finalizes the file editing/writing.

It is important to note that opening a file doesn’t alter the file until you call .close(). This prevents files from being half-written.

Listing All Saved Files in OPFS

Add the following to your app.html file:

async function loadNotesList() {
  try {
    const root = await getOPFSRoot();
    notesListEl.innerHTML = "";
    for await (const [name, handle] of root.entries()) {
      const li = document.createElement("li");
      li.innerHTML = `
                <strong>${name}</strong>
                <button onclick="loadNote('${name}')">Load</button>
                <button onclick="deleteNote('${name}')">Delete</button>
              `;
      notesListEl.appendChild(li);
    }
    showStatus("Notes list updated.");
  } catch (err) {
    console.error(err);
    showStatus("Error loading notes list.");
  }
}

To list all saved files in OPFS, we must again get access to the root directory of OPFS via our function getOPFSRoot(), which is where all files are stored. Next, we loop through all the files and folders in the root directory, and each entry in the loop has a name and a handle. The handle is the object that you can use to read/load or write to the file.

At this point, we just loop through every file in the OPFS directory and display them all on our Saved Notes section.

Now I will attempt to save two notes to the OPFS.

Saving note files in OPFS and listing them

Reading a File from OPFS

Add the following to your app.html file:

async function loadNote(title) {
  try {
    const root = await getOPFSRoot();
    const handle = await root.getFileHandle(`${title}`);
    const file = await handle.getFile();
    const content = await file.text();
    titleEl.value = title.replace(".txt", "");
    noteEl.value = content;
    showStatus(`Note '${title}' loaded.`);
  } catch (err) {
    console.error(err);
    showStatus("Error loading note.");
  }
}

To read a file means to load the file and access its content, usually for displaying or editing. In our case, we do that using a helper function called loadNote(title), where title is the name of the note (i.e., the filename).

We proceed to obtain the root directory handle again using getOPFSRoot() and get the file handle reference of the file we want to read with getFileHandle(title), where title is the name of the note.

We open the file with getFile() and then use file.text() to parse the saved note as a string so we can copy that content to a variable.

We finally populate the corresponding value field so the user can view or edit the note.

Let’s go ahead and read the two files we created back to the UI.

Reading a saved note file from OPFS

Deleting a File from OPFS

Add the following to your app.html file:

async function deleteNote(title) {
  if (!confirm(`Delete note '${title}'?`)) return;
  try {
    const root = await getOPFSRoot();
    await root.removeEntry(`${title}`);
    showStatus(`Note '${title}' deleted.`);
    loadNotesList();
  } catch (err) {
    console.error(err);
    showStatus("Error deleting note.");
  }
}

Deleting a file from OPFS means removing the file completely from OPFS. To do that, we created a helper function named deleteNote(title). The helper function first implements a standard reconfirmation prompt using the browser’s confirm() function to avoid unintentional deletion, because without it OPFS will delete the file without warning.

We gain access to our root directory and use the reference handle to call removeEntry(title), which deletes the file provided as a parameter.

Let’s try to delete both saved files from OPFS.

Deleting saved notes from OPFS

Putting It All Together

Refresh the displayed saved notes by calling loadNotesList() whenever any manipulation happens to the OPFS. This helps prevent files being duplicated.

Your app.html file should look like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini Notepad (OPFS)</title>
    <style>
      body {
        font-family: sans-serif;
        padding: 2rem;
        max-width: 600px;
        margin: auto;
      }
      input,
      textarea {
        width: 100%;
        margin-bottom: 1rem;
        padding: 0.5rem;
      }
      button {
        margin-right: 0.5rem;
        margin-top: 0.5rem;
        padding: 0.5rem 1rem;
      }
      .status {
        margin-top: 1rem;
        color: green;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      li {
        margin-bottom: 0.5rem;
      }
    </style>
  </head>
  <body>
    <h1>Mini Notepad (OPFS)</h1>
    <input type="text" id="title" placeholder="Enter note title..." />
    <textarea id="note" placeholder="Compose your note here..."></textarea>
    <button onclick="saveNote()">Save Note</button>
    <button onclick="loadNotesList()">Refresh Notes List</button>
    <section>
      <h2>Saved Notes</h2>
      <ul id="notes-list"></ul>
    </section>
    <div class="status" id="status"></div>
    <script>
      const notesListEl = document.getElementById("notes-list");
      const statusEl = document.getElementById("status");
      const titleEl = document.getElementById("title");
      const noteEl = document.getElementById("note");
      async function getOPFSRoot() {
        if (!("storage" in navigator && "getDirectory" in navigator.storage)) {
          alert("OPFS is not supported in this browser.");
          throw new Error("OPFS not supported");
        }
        return await navigator.storage.getDirectory();
      }
      async function saveNote() {
        const title = titleEl.value.trim();
        const content = noteEl.value;
        if (!title) return showStatus("Please enter a title.");
        try {
          const root = await getOPFSRoot();
          const handle = await root.getFileHandle(`${title}.txt`, {
            create: true,
          });
          const writable = await handle.createWritable();
          await writable.write(content);
          await writable.close();
          showStatus(`Note '${title}' saved.`);
          loadNotesList();
        } catch (err) {
          console.error(err);
          showStatus("Error saving note.");
        }
      }
      async function loadNotesList() {
        try {
          const root = await getOPFSRoot();
          notesListEl.innerHTML = "";
          for await (const [name, handle] of root.entries()) {
            const li = document.createElement("li");
            li.innerHTML = `
                <strong>${name}</strong>
                <button onclick="loadNote('${name}')">Load</button>
                <button onclick="deleteNote('${name}')">Delete</button>
              `;
            notesListEl.appendChild(li);
          }
          showStatus("Notes list updated.");
        } catch (err) {
          console.error(err);
          showStatus("Error loading notes list.");
        }
      }
      async function loadNote(title) {
        try {
          const root = await getOPFSRoot();
          const handle = await root.getFileHandle(`${title}`);
          const file = await handle.getFile();
          const content = await file.text();
          titleEl.value = title.replace(".txt", "");
          noteEl.value = content;
          showStatus(`Note '${title}' loaded.`);
        } catch (err) {
          console.error(err);
          showStatus("Error loading note.");
        }
      }
      async function deleteNote(title) {
        if (!confirm(`Delete note '${title}'?`)) return;
        try {
          const root = await getOPFSRoot();
          await root.removeEntry(`${title}`);
          showStatus(`Note '${title}' deleted.`);
          loadNotesList();
        } catch (err) {
          console.error(err);
          showStatus("Error deleting note.");
        }
      }
      function showStatus(msg) {
        statusEl.textContent = msg;
      }
      loadNotesList();
    </script>
  </body>
</html>

Storage Concerns

It is important to note that while OPFS may be generous with its storage limits, you should only store what you need, as the browser may throw a QuotaExceededError if your application gets close to the dynamic storage quota.

Conclusion

The Origin Private File System represents significant progress in structured browser storage technology. It gives web developers the ability to create, read, write and manage large files of various types directly on a user’s device without relying on server storage.

As we have seen from what we built in this article, OPFS is the solution for whatever web application you may be building that requires structured, persistent, secure, origin-scoped and offline file access.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.