Skip to content
FrameworkJavaScript

The BlokEditor React component

The all-in-one editor component shipped by the framework adapters. It is <BlokEditor> in @bloklabs/react and @bloklabs/vue, and <blok-editor> (BlokEditorComponent) in @bloklabs/angular.

React and Vue accept every editor config option as a prop. They forward unknown props and attributes to the container div.

Angular works differently. It declares a fixed set of @Input()s: tools, data, readOnly, hideToolbar, toolbarPosition, inlineToolbar, theme, width, placeholder, styleTokens, i18n, autofocus, migrations, onBeforeRender, onBeforePaste and onError.

Every other config key goes through the [config] escape hatch. That covers sanitizer, minHeight, defaultBlock, dataModel, link, linkPaste, tunes, user, resolveUser, uploader, server, ticket, persistence, collaboration, notifier, logLevel, onEnter, onSubmit, scrollToBlock and so on. Angular also does not forward host attributes onto the container div.

You read the live Blok instance in each adapter.

  • In React use ref/onReady.
  • In Vue use the instance on a template ref or the @ready emit.
  • In Angular use the instance signal or the (ready) output.

The props below cover the adapter-specific surface. Everything else matches the Configuration options.

Last updated Jul 17, 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

useBlok(config, deps?)

Blok | null (React) | Ref<Blok | null> (Vue)

The split mount path behind <BlokEditor>: you create the instance yourself and hand it a mount point. useBlok takes the SAME options as the component. Its config type is UseBlokConfig, which is BlokConfig minus holder (the adapter owns the mount element) plus an adapter-level width.

UseBlokConfig itself documents the subset that stays reactive after mount: readOnly, hideToolbar, toolbarPosition, inlineToolbar, autofocus, theme, width, placeholder, style.tokens, i18n and data sync in place on the same instance. Every other option is read once at editor creation.

It returns null until the editor exists (SSR, first render).

  • React takes a deps dependency LIST as the second argument.
  • Vue takes a reactive config source (ref or getter) and a SINGLE recreateKey as the second argument.
  • The Angular equivalent is the [blokContent] directive (BlokContentDirective).
    • It builds the instance into its own host element and exposes it as the instance signal / (ready) output.
TypeScript
import { useBlok, BlokContent } from '@bloklabs/react';
import { Header, Paragraph } from '@bloklabs/core/tools';

export function Editor() {
  const editor = useBlok({
    tools: { paragraph: Paragraph, header: Header },
    readOnly: false,
  });

  return <BlokContent editor={editor} className="my-editor" />;
}

BlokContent

React/Vue component

The mount point for an instance created by useBlok. It renders a <div> and adopts the editor's detached holder into it.

Its only own prop is editor: Blok | null (BlokContentProps). Pass null before the instance exists and it renders the empty container.

In React it also extends React.HTMLAttributes<HTMLDivElement>, so className, id and the rest are forwarded to that div, and it forwards a ref to it. The Angular counterpart is the [blokContent] directive, which creates the instance itself instead of receiving one.

TypeScript
import { useBlok, BlokContent } from '@bloklabs/react';

const editor = useBlok({ tools });

// `editor` is null until the instance exists — BlokContent handles that
<BlokContent editor={editor} className="prose" />

provideBlok(defaults)

void | EnvironmentProviders

Registers app-wide Blok defaults. Every editor beneath it then inherits a shared tools registry, theme or i18n config instead of repeating them per instance.

  • React spells it as <BlokProvider defaults={…}>, with useBlokDefaults() to read them back.
  • Vue spells it as provideBlok(defaults) called in a parent's setup, backed by the BLOK_DEFAULT_CONFIG injection key, with useBlokDefaults() to read.
  • Angular spells it as provideBlok(defaults) returning EnvironmentProviders for a providers array, backed by the BLOK_DEFAULT_CONFIG injection token.

The merge rule is identical in all three: a defined per-instance config value overrides the default. tools is the exception. There the two registries are merged, so the shared registry composes with per-instance additions instead of being replaced.

TypeScript
// React
import { BlokProvider } from '@bloklabs/react';

<BlokProvider defaults={{ theme: 'dark', tools: sharedTools }}>
  <App />
</BlokProvider>

// Vue — inside a parent component's setup()
import { provideBlok } from '@bloklabs/vue';
provideBlok({ theme: 'dark', tools: sharedTools });

// Angular
import { provideBlok } from '@bloklabs/angular';
bootstrapApplication(AppComponent, {
  providers: [provideBlok({ theme: 'dark', tools: sharedTools })],
});
TypeScript
import { useState } from 'react';
import { BlokEditor } from '@bloklabs/react';
import { Header, Paragraph, List } from '@bloklabs/core/tools';
import type { OutputData } from '@bloklabs/core';

export function Editor() {
  const [data, setData] = useState<OutputData>();

  // data + onSave form a controlled component: onSave fires (debounced)
  // with the serialized document; echoing it back is deduped and
  // caret-stable, while genuine external data changes re-render in place.
  return (
    <BlokEditor
      tools={{ paragraph: Paragraph, header: Header, list: List }}
      data={data}
      onSave={setData}
      theme="auto"
      className="my-editor"
    />
  );
}

BlokEditor component

PropertyDescription
toolsRecord<string, ToolConstructable | ToolSettings>

Block tools to register. React only: functions anywhere inside a tool's config (for example an uploader callback) are re-bound to the latest render automatically. So inline closures are safe, and only a changed tool CLASS needs a deps entry.

Vue and Angular have no equivalent. There a closure in a tool config is captured when the editor is constructed, and then goes stale. Keep it in a stable ref/field, or force a rebuild by changing recreateKey.

dataOutputData | LooseOutputData | null

Editor content (reactive). It seeds the initial document. After mount, new content re-renders in place on the same instance and never recreates the editor. That includes transitions to and from empty content.

Updates are deduped with the same structural lens as equalsOutputData, so echoing the editor's own output back never clobbers the caret. That holds even after a persistence layer strips it: a fresh time/version, dropped ids and a missing lastEditedAt stamp still count as unchanged.

Imperative content calls stay in the same world. React's useBlokHandle().clear() / .render() and Angular's BlokEditorComponent.render() update that baseline once they land. So setting data back to a document the editor itself emitted re-renders it instead of being dismissed as an echo.

A whole-document null is the controlled "clear to empty" value. Route it through toRenderableData when you call render() yourself.

Loose backend DTOs are accepted as-is. Angular widens the type to … | undefined. Vue's prop is declared PropType<OutputData> and is the narrow outlier.

onSave(data: OutputData, api: API) => void

The output half of the controlled component. It fires (debounced) with the full serialized document on every content change, so there is no manual save() polling. Wiring onSave={setData} is safe and recursion-free.

  • The (data, api) arity is React's, where the prop is passed straight through to the core config.
  • Vue maps it to the save emit and Angular to the save output.
    • Both carry OutputData only: @save="(data) => …" / (save)="…".

You can also use v-model:data / [(data)], backed by Vue's update:data emit and Angular's dataChange output.

onChange(api: API, event: BlockMutationEvent | BlockMutationEvent[]) => void

Low-level mutation events (block added/changed/moved/removed). Use it when you need per-mutation granularity instead of serialized output. A batch of mutations arrives as an ARRAY, so branch on Array.isArray(event) before reading event.detail.

The two positional arguments are React's arity. Vue's change emit and Angular's change output deliver ONE object instead: @change="({ api, event }) => …" / (change)="…" with $event.api and $event.event.

onReady(editor: Blok) => void

Called with the live Blok instance, exactly once per editor instance. It fires after the forwarded ref commits, so ref.current is also populated.

The editor is recreated, and onReady fires again, only when deps/recreateKey change or the component remounts. Changes to data, including to and from empty content, re-render in place and never re-fire it.

Vue and Angular spell it as the ready emit/output (@ready / (ready)).

deps / recreateKeyDependencyList (React) | unknown (Vue, Angular)

Values whose identity change destroys and recreates the editor. Use it for structural config like tool classes.

  • React takes an array, deps, and recreates when any entry's identity changes.
  • Vue (:recreate-key) and Angular ([recreateKey]) take a SINGLE value instead.
    • They recreate when that value's identity changes, so pass a fresh object/array literal or a bumped counter.
    • deps does not exist on Vue/Angular.

Keep each value referentially stable. Functions inside tool configs do NOT belong here on React. They are re-bound to the latest render automatically.

readOnlyboolean | ReadOnlyModeConfig

Read-only mode. Reactive: toggles in place after mount, without remounting.

theme'light' | 'dark' | 'auto'

Color theme (reactive). Don't wrap the component in styled() or any HOC that reserves the theme prop. It would never reach the editor.

onThemeChange(resolvedTheme: 'light' | 'dark') => void

Called with the resolved theme whenever it changes, for example when 'auto' follows the OS. Vue and Angular spell it as the theme-change emit / themeChange output (@theme-change / (themeChange)).

width'narrow' | 'full'

Content width mode (reactive). It is synced after mount via editor.width.set(). See the Width API for the imperative surface (get / set / toggle).

styleBlokConfig['style']

Styling config. style.tokens is reactive. Changed --blok-* overrides sync in place after mount via editor.tokens.set(), deduped by deep equality, so a host light/dark toggle needs no remount.

It replaces rather than merges: pass the whole palette, because tokens dropped from it stop applying.

Angular has no style input. It exposes only style.tokens, as the separate [styleTokens] input (Record<string, string>). The remaining style keys, fontSize, contentAlign and nativeSelection, must go through Angular's [config] escape hatch.

i18nBlokConfig['i18n']

Internationalization config (reactive). A changed locale, messages or direction syncs in place after mount via editor.i18n.update(), deduped by deep equality. So a language switcher relabels the editor without remounting it, and caret, focus, selection and undo history survive.

defaultLocale is the exception: it is read only at construction. Angular exposes this as the [i18n] input.

localestring

React only. A library-neutral BCP-47 shorthand for i18n.locale. It is folded into the i18n config and applied in place via editor.i18n.update({ locale }), so a language switch keeps caret, focus and undo history.

When both are given, it WINS over i18n.locale. Pair it with getDirection / normalizeLocale, re-exported from @bloklabs/react, to compute dir and validate tags yourself.

Vue and Angular have no such prop. There you pass the locale inside the i18n prop/input.

autofocusboolean

Focus the editor after it mounts.

placeholderstring | false

Placeholder text handed to every block of the default tool, not only the first block. With the built-in paragraph it shows while a block is empty and focused. See the Configuration table for what false does and does not disable. See the Placeholder API to change it at runtime via editor.placeholder.set().

onBlocksRendered(payload: BlocksRenderedPayload) => void

Called after a batch render completes (the core blocks:rendered event). It is the declarative analog of editor.on('blocks:rendered', …). Vue and Angular spell it as the blocks-rendered emit / blocksRendered output.

onBlockRendered(payload: BlockRenderedPayload) => void

Called for each block rendered into the DOM (core block:rendered event). Vue and Angular spell it as the block-rendered emit / blockRendered output.

refRef<Blok | null>

Forwarded to the live Blok instance for imperative calls (save, render, blocks, caret, …). It is null until the editor mounts, so calls must guard on ref.current.

For the common shortcuts without the guards, @bloklabs/react's useBlokHandle() returns a stable, null-safe handle. Attach it via ref={handle.ref} and call handle.focus()/save()/clear()/render()/setReadOnly() directly. Each one safely no-ops until ready, and handle.current is the escape hatch to the full instance.

When you also drive content through the data prop, prefer the handle for clear()/render(). It updates the controlled baseline. A raw ref.current.render()/clear() changes the content behind the adapter's back, and a later data value equal to what the editor emitted before is then deduped away.

className, id, …HTMLAttributes<HTMLDivElement>

Any prop that is not an editor config option is forwarded to the container div. Style the editor through className (style keeps its editor-config meaning).