Skip to content
FrameworkJavaScript

Title API: the page title and icon

Read and change the page title and its icon. They sit above the first block, or in an element of your own. The title is one line of plain text. Both are stored in the document and saved as title and icon in OutputData.

Turn it on with the pageTitle option. editor.title exists right after new Blok(), and calls made before isReady are replayed once the editor is ready. title.focus() is the exception: before isReady it does nothing.

The opt-in title: true option carries the title through conversions. Without it, every conversion works as it did before.

  • blocksToHtml writes it first as an <h1>, led by the icon.
  • blocksToMarkdown, blocksToMarkdownWithReport and blocks.exportMarkdown write it as a # heading. An image icon is left out.
  • blocksToPlainText writes it as the first line.
  • markdownToBlocksWithReport and htmlToBlocksWithReport lift a leading # heading or <h1> out of the blocks. They return it as title and icon. A heading that starts with an emoji and a space comes back as the icon plus the title.
    • The title is plain text, so formatting in that heading is dropped. Both reports add a heading warning for it.
    • markdownToBlocks and htmlToBlocks with the option only drop the heading.
  • blocks.importMarkdown sets the editor's title and icon from that heading. A heading with no emoji clears the icon, and no heading clears both.
  • extractTexts puts the title first in the list, and injectTexts writes it back from there. Every other index moves by one. A blank translation removes the title.

These limits are known.

  • Restoring a version saved before the server kept page titles clears the current title.
  • A collaborative editor that opens offline shows its last-known blocks, but no title.
  • A host cannot set the title through the server's /edit endpoint.
  • When two people type in the title at once, the last write wins.
Last updated Oct 8, 2026Edit this page on GitHub

Reaching the editor instance

The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.

TypeScript
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');

Methods

title.get()

string

The current title. It is '' when there is none.

TypeScript
console.log(editor.title.get()); // 'Roadmap'

title.set(text, options?)

void

Replaces the title. It makes one undo step and calls onChange with source api. Setting the text the title already has does nothing.

Pass { record: false } for a write that should add no undo step, such as a rename that came from your own backend. Peers still get the write, and onChange gets { source: 'api', record: false }.

  • Undo does not go past this value until the next recorded title change. After that, undo can bring back a title from before it.
  • To make the value a floor, call history.clear() right after the write.
  • A typing run that goes on after it stays one undo step. Undo goes back to the value you wrote, not to the value before the run.

Parameters

ParameterTypeRequiredDefaultDescription
textstringRequired—

The new title. '' clears it.

options.recordboolean—true

false: no undo step. Peers still get the write.

TypeScript
editor.title.set('Roadmap 2027');

// A rename from your backend: no undo step
editor.title.set(serverTitle, { record: false });

title.focus(position?)

void

Puts the caret in the title, at the end by default. Pass 'start' for the start. It does nothing when the title is off, and before isReady.

TypeScript
editor.title.focus('start');

title.mount(holder)

void

Moves the title and icon into holder, an element or a selector. Focus and the caret are kept. null puts the title back above the first block.

After isReady, a selector that matches nothing throws. Before isReady the call waits for the editor. A bad selector is then logged, and the title stays where it is.

To line up a section of your own between the title and the editor, copy the title's layout. Its --blok-content-column variable is set on the title's own element, so a section beside it cannot read it.

  • Pad the section's start by the editor gutter, 56px by default.
    • It is 0 with hideToolbar: true or readOnly: { hideControls: true }.
    • toolbarPosition: 'right' moves it to the end.
  • Inside it, give the content box-sizing: border-box, max-width: var(--blok-content-max-width, 720px) and padding-inline: var(--blok-block-padding-inline, 2px).
  • In full width, use none instead of 720px.
  • Align it as style.contentAlign says.
    • The default 'left' needs no margin.
    • 'center' needs margin-inline: auto.
    • 'right' needs margin-inline-start: auto.

The section reads these variables only from its own ancestors. Blok applies style.tokens to its own elements only, so a section beside the title does not see them.

  • If you set --blok-content-max-width, declare it on an element that holds both the section and the editor.
  • Otherwise the section falls back to 720px and drifts away from the title.

The gutter works differently. Blok declares --blok-editor-gutter-start on the editor's own element, so a value on an ancestor does not reach the editor. style.tokens skips gutter variables with a warning.

  • Change the gutter with a CSS rule on the editor, such as [data-blok-interface] { --blok-editor-gutter-start: 20px; }.
  • The section cannot read that rule. Give it the same value yourself, written out or as the same variable on one of its own ancestors.
TypeScript
editor.title.mount('#page-title');

// A section of your own between the title and the editor
// .page-meta { box-sizing: border-box; padding-inline-start: 56px; }
// .page-meta > div {
//   box-sizing: border-box;
//   max-width: var(--blok-content-max-width, 720px);
//   padding-inline: var(--blok-block-padding-inline, 2px);
// }

// Back above the first block
editor.title.mount(null);

title.icon.get()

PageIcon | null

The current icon: { type: 'emoji', value } or { type: 'image', url }. It is null when there is none.

TypeScript
const icon = editor.title.icon.get(); // { type: 'emoji', value: '🗺️' }

title.icon.set(icon, options?)

void

Sets the icon, or removes it with null. It makes one undo step and calls onIconChange with source api. { record: false } works as it does for title.set.

TypeScript
editor.title.icon.set({ type: 'emoji', value: '🚀' });
editor.title.icon.set(null);
TypeScript
import { Blok } from '@bloklabs/core';

const editor = new Blok({
  holder: 'editor',
  pageTitle: {
    placeholder: 'Untitled',
    onChange: (title, change) => {
      if (change.source !== 'remote') {
        saveTitle(title);
      }
    },
  },
  data: { title: 'Roadmap', icon: { type: 'emoji', value: '🗺️' }, blocks: [] },
});

editor.title.get(); // 'Roadmap'

const saved = await editor.save();
// saved.title === 'Roadmap'

Title API

PropertyDescription
holderHTMLElement | string

Element or selector to draw the title in. Without it, the title sits above the first block.

placeholderstring

Shown while the title is empty. The default is the localized title.placeholder message.

iconboolean

false hides the icon and the Add icon button.

onChange(title: string, change: TitleChange) => void

Called on every title change, from any source. change.source is user, undo, redo, remote or api. Save on user, undo, redo and api.

change.record is false only for a write made with { record: false }. Otherwise the key is absent.

onIconChange(icon: PageIcon | null, change: TitleChange) => void

Called on every icon change, from any source. null means the icon was removed. change is the same as for onChange.