Skip to content
FrameworkJavaScript

Tab sync: one document in every tab

Keep one document live across the tabs of one browser, with no server.

Last updated Oct 6, 2026Edit this page on GitHub

What it does

Open the same document in two tabs of one browser. A change in one tab shows up in the others. No server is involved.

Tab sync is on by default. tabSync: false turns it off. tabSync: { settings: false } keeps the document in sync but stops sharing locale, theme mode and width.

With collaboration set, tab sync stays off. The collaboration server already keeps every tab in sync.

Where the document id comes from

Tabs find each other by a document id. Pass it as documentId. Use the id your app already loads and saves the document by:

  • A route param, such as /notes/:id.
  • The id inside your persistence URLs.
  • The key of the database record.
  • A fixed name, such as 'notes', when the site has a single editor.
  • A new unique id for a new document.

For a new id, crypto.randomUUID() works on HTTPS and on localhost. It is missing on a plain-http page, because it needs a secure context. Any unique string works just as well.

The id must be unique across the whole site. If two of your customers can share an id, add a prefix, such as 'acme:42'. An empty string is refused, and isReady rejects.

Without documentId, Blok uses the id it writes into saved data, plus the page path. That covers a document that persistence loads. A document with no stored id joins after its first persistence save.

A document passed as data with its own id never joins on its own. Neither does one saved only through onSave, or an empty one. Pass documentId in those cases.

What syncs

These follow across the tabs that show the same document:

  • The document content.
  • The content width.
  • Whether each toggle is open. This is personal to the browser and never saved in the document.

Locale and theme mode follow across every tab of the same origin that runs Blok. Recent colors, links and code languages, the emoji skin tone, and media volume, speed and loop are stored by the browser. They are shared whatever tabSync says.

These never sync:

  • Selection and caret.
  • Scroll position.
  • Open menus and popovers.
  • Find in page.
  • Read-only mode.
  • Theme tokens and other host config.
  • Media playback position.

The tab you work in saves

Only one tab saves at a time: the tab you are working in. It calls persistence.save and onSave, including the save handlers inside the framework adapters. Tabs in the background are passive.

When you switch to another tab, that tab takes over, usually within half a second. The tab you left first saves what it still holds. If a save is still running, the new tab waits for it, which can take a few seconds.

So a binding that rides onSave stays current in the tab you use:

  • Vue v-model:data.
  • Angular [formControl].
  • A React onSave that feeds your state.

In a background tab such a binding can lag. To read the document there, use editor.save().

A read-only tab never saves. When the saving tab closes, another open tab takes over.

onChange still fires in every tab. Its events carry origin, which is 'tab' for a change made in another tab.

Toggles

Whether a toggle is open is personal. Each browser remembers its own state, and it is never saved in the document. The same holds for toggleable headings.

  • A toggle starts collapsed.
  • A toggle you create starts open for you in this browser.
  • With tab sync on, opening a toggle in one tab opens it in the other tabs of this browser that show the same document.

The state is keyed by documentId, the collaboration doc, or else the document's id. If your app drops id when it stores the document, the state is lost on reload. Copies that share an id share the state too.

isOpen is no longer saved or read. A document saved with isOpen opens with its toggles collapsed. The state lives in api.viewState under the key 'open'.

Limits

The document stays separate in each tab in these cases:

  • Inside a third-party iframe in Chrome and Firefox. Those browsers split tab channels by the top-level site.
  • On a plain-http page. Web Locks need a secure context, and localhost counts as one.
  • Across Blok versions that use different tab sync protocols.
  • In a tab where the user typed before it joined. It stays separate until reload.

A separate tab saves on its own, next to the tab that saves. It never takes over saving, even while you work in it. Only a persistence whose load returns a version can catch that clash.

A tab opened while the saving tab is frozen in the background works on its own. It joins when that tab wakes, unless the user typed in it meanwhile.

If the saving tab is frozen when you switch to another tab, the tab you switch to takes over after about 4 seconds.

Typing reaches the other tabs within about 400 ms.

If your app copies stored documents and opens every document at one path, the copies share an id. Pass documentId, or remove id when you copy.

documentId and tabSync are fixed for the editor's life. To switch documents, recreate the editor. In React, change its key or the useBlok deps. In Vue and Angular, change recreateKey.