Перейти к содержимому
ФреймворкJavaScript

Styles API: классы для ваших инструментов

Доступ к CSS-классам для стилизации пользовательских инструментов и элементов интерфейса, а также настройка макета и обрамления редактора через публичные CSS-переменные. Основной способ переопределить токены темы — `style.tokens` в конфигурации конструктора Blok: передайте ключи `--blok-*` со значениями, и Blok внедрит таблицу стилей для экземпляра, которая автоматически достаёт и до редактора, И до интерфейса, портированного в `document.body` (поповеры, подсказки, элементы верхнего слоя); некорректные ключи пропускаются с предупреждением, а таблица стилей удаляется при destroy. Внедрённые значения `style.tokens` статичны в рамках приложения — они применяются одинаково в светлой и тёмной темах и в режиме только для чтения, поэтому зависящим от состояния токенам вроде бокового отступа редактора (gutter) место в CSS; `style.tokens` игнорирует ключи `--blok-editor-gutter-*` с предупреждением. Но они не зафиксированы на момент создания: `editor.tokens.set(tokens)` переписывает внедрённую таблицу стилей на лету — именно это нужно переключателю светлой/тёмной темы на стороне хоста; без него смена токена означала бы пересоздание редактора или ручное написание глобальной таблицы стилей, нацеленной на области портала. `set()` принимает полный набор токенов (замена, а не слияние), повторяя семантику `style.tokens`, поэтому токены, отсутствующие в новой палитре, перестают действовать, а `{}` удаляет таблицу стилей; `editor.tokens.get()` возвращает то, что применено сейчас. API доступен синхронно сразу после создания (вызовы до `isReady` буферизуются и воспроизводятся), а адаптеры React/Vue/Angular управляют им реактивно — передайте `style={{ tokens }}` (React/Vue) или `[styleTokens]` (Angular), и изменения синхронизируются на месте без пересоздания редактора. Как альтернатива на чистом CSS: собственная палитра Blok объявлена с нулевой специфичностью через `:where()`, поэтому один обычный селектор вида `[data-blok-interface] { --blok-popover-bg: … }` выигрывает независимо от порядка таблиц стилей — но, так как поповеры портируются в `document.body`, эта глобальная таблица стилей должна дополнительно нацеливаться на `[data-blok-popover], [data-blok-top-layer]`, чтобы достать до них. `--blok-content-max-width` остаётся определяющим в обоих режимах ширины — `width='full'` лишь подменяет его запасное значение на `none`. В режиме редактирования Blok автоматически резервирует 56px бокового отступа под плавающие элементы управления блоком +/⠿, а на обёртке появляется `data-blok-readonly`, пока активен режим только для чтения. Обычный режим только для чтения СОХРАНЯЕТ этот отступ: в нём живёт элемент «скопировать ссылку», появляющийся при наведении на блок, а `readOnly.set()` переключает режимы на месте — схлопывание отступа сдвигало бы документ вбок при каждом переключении. Отступ автоматически схлопывается до 0 только там, где он действительно мёртвое пространство: в режиме только для чтения без элементов управления (`readOnly: { hideControls: true }`, обёртка получает `data-blok-controls-hidden`) и при `hideToolbar: true` в конфигурации конструктора — панель инструментов при наведении тогда не открывается, обёртка получает `data-blok-toolbar-hidden`, и место под отступ не резервируется. `--blok-editor-gutter-start` — это точка переопределения, а не обязательное заклинание: задайте ему любое значение (в том числе `0px`, чтобы убрать отступ), чтобы изменить значение по умолчанию. Контракт переопределения отступа гарантирован, а не случаен: Blok объявляет и значение по умолчанию, и оба схлопывания по состоянию с нулевой специфичностью через `:where()` (это закреплено контрактным юнит-тестом), поэтому объявление токенов отступа на стороне хоста с любой положительной специфичностью всегда выигрывает каскад. Объявляйте их на самом элементе-обёртке (например, `[data-blok-interface] { --blok-editor-gutter-start: 16px }`), а не только на предке: схлопывания controls-hidden и toolbar-hidden переобъявляют эти токены на обёртке, а пользовательские свойства разрешаются из ближайшего объявления, поэтому значение на уровне предка проиграет схлопыванию, а значение на уровне обёртки его переживёт. Горизонтальное положение колонки контента тоже настраивается на уровне API — через `style.contentAlign?: 'left' | 'center' | 'right'` (по умолчанию `'left'`) в конфигурации конструктора Blok. Blok также перекрашивает нативное выделение текста внутри редактора через `--blok-selection-inline` — переопределите этот токен, чтобы изменить цвет, или передайте `style.nativeSelection: true` (по умолчанию `false`), чтобы полностью отказаться от этого и вернуться к цветам выделения браузера/хоста (переопределение токена не может выразить общие CSS-ключевые слова вроде `revert`, поэтому для возврата нужен именно этот флаг). С включённым флагом обёртка получает `data-blok-native-selection`, правила `::selection` от Blok обходят редактор, а подсветка fake-background (показываемая, пока фокус удерживает поле ввода в меню) следует цвету `Highlight` из браузера; в поповерах цвет выделения от Blok сохраняется. Фоновые поверхности — тоже публичные токены: большинство светлых поверхностей и поверхностей при наведении следуют `--blok-bg-light`, карточки пустого состояния медиа используют `--blok-bg-secondary` (с границей `--blok-border-secondary`), а скелетоны загрузки изображений/файлов и плейсхолдеры загрузки — `--blok-bg-tertiary`, который по умолчанию равен `--blok-bg-light` и потому следует за темой; перекрасить поверхность скелетона — значит переопределить `--blok-bg-tertiary` напрямую, а не перегружать `--blok-bg-light`, утягивая за собой все остальные поверхности. Как и все цветовые токены палитры, токены поверхностей переобъявляются самим Blok на обёртке редактора с нулевой специфичностью, поэтому применяйте переопределения через `style.tokens` / `editor.tokens.set()` либо через CSS-селектор, попадающий в саму обёртку (`[data-blok-interface]`): объявление пользовательского свойства на контейнере-предке перекрывается собственным объявлением обёртки и молча ничего не делает (компоновочные хуки вроде `--blok-content-max-width`, а также токены списков, заголовков, embed, отступов блока и цвета плейсхолдера, наоборот, читаются с запасными значениями и никогда не объявляются самим Blok — поэтому они ДЕЙСТВИТЕЛЬНО наследуются от любого предка; токены бокового отступа и `--blok-search-input-placeholder` объявляются на обёртке, как палитра, поэтому им тоже нужно правило уровня обёртки). Учтите также, что внедряемые таблицы токенов нацелены на атрибуты области Blok глобально: если на странице несколько экземпляров редактора, таблица стилей `style.tokens` / `tokens.set()` каждого экземпляра применяется ко ВСЕМУ интерфейсу Blok на странице, а не только к своему экземпляру (каждая удаляется при уничтожении своего экземпляра; при конфликте наборов между экземплярами решает порядок таблиц стилей в `<head>`, а не давность применения, поэтому давайте всем экземплярам один общий набор вместо расчёта на порядок конфликта) — различия между экземплярами задавайте CSS-правилом на собственной обёртке каждого редактора (интерфейс поповеров, смонтированный в body, всегда следует общестраничным таблицам). Таблицы внедряются в начало `<head>`, поэтому правило таблицы стилей хоста с равной специфичностью — обычное `[data-blok-interface] { … }` — всё равно побеждает `style.tokens` для объявленных в нём токенов. Ритм блоков тоже публичен: `--blok-block-padding-top`, `--blok-block-padding-bottom` и `--blok-block-padding-inline` задают внутренние отступы обёртки любого блочного инструмента (параграф, заголовок, список, тоггл, цитата). Каждый инструмент сохраняет своё историческое значение как запасное — 7px/7px/2px у большинства блоков, 0.2em по вертикали у цитат — поэтому одно переопределение перенастраивает все блоки сразу; именно это нужно хосту в режиме только для чтения для плотной строчной вёрстки (раньше это было возможно только переопределением внутренностей `[data-blok-tool]`). Панель выноски — намеренное исключение: внутренний отступ её карточки задаётся `--blok-callout-padding-block` (по умолчанию 5px), а НЕ токенами ритма, поэтому уплотнение ритма не может схлопнуть карточку выноски на её текст — при этом эмодзи остаётся на первой строке текста, потому что его кнопка следует за `--blok-block-padding-top` вместе с дочерним текстом. Учтите, что нестандартные отступы слегка сдвигают производную геометрию — например, смещение стрелки заголовка-тоггла, которое следует за `--blok-block-padding-top`. Раскладка колонок публична точно так же: строка колонок — это `[data-blok-columns]`, а холдер каждой колонки — один из её непосредственных дочерних `[data-blok-element]` (строка в режиме только для чтения дополнительно несёт `data-blok-columns-static-gutter`, поскольку в опубликованных строках зазор задаёт контейнер, а не разделители `[data-blok-column-resizer]`, которые существуют только во время редактирования). `--blok-column-gutter` задаёт этот зазор (по умолчанию `min(2rem, 4vw)`), а `--blok-column-min-width` — насколько сильно можно сжать колонку (по умолчанию `0`, то есть колонку можно перетащить до полного схлопывания). Нижний предел соблюдают И раскладка, И перетаскивание разделителя — при нажатии указателя перетаскивание считывает вычисленное значение обратно, — поэтому его повышение останавливает ручку на этом пределе, а не сохраняет ширину, которую раскладка откажется отрисовывать. Вложенность блоков публична так же: блок, вложенный в другой (Tab на верхнем уровне), получает отступ `--blok-block-indent-step` на каждый уровень (по умолчанию `24px`), и это настоящий CSS, а не инлайновый стиль, поэтому обычное правило хоста меняет или убирает его без `!important`. Blok обнуляет шаг внутри каждого дочернего слота `[data-blok-nested-blocks]` — этот маркер рендерит для своих детей любой контейнерный инструмент, встроенный и сторонний, — поэтому блоки, которые контейнер уже расположил сам, не сдвигаются дополнительно по своей глубине; контейнеру, которому отступ нужен, достаточно объявить шаг обратно на своём слоте. Этот сброс держится на наследовании, а не на проверке в JS, именно затем, чтобы он действовал и для слота, созданного уже после вставки дочернего блока, — а именно так поступает портал адаптера фреймворка. Размер текста публичен и для блока, И для сценария через `style.fontSize` — это поддерживаемая альтернатива обращению к внутренним именам классов Blok. Каждый ключ пишет один публичный токен: `fontSize.paragraph` → `--blok-paragraph-font-size`, `fontSize.heading[1]` → `--blok-heading-1-font-size` (заголовки переиспользуют уже существующие токены заголовков, а не заводят параллельные), `fontSize.list.checklist` → `--blok-checklist-font-size`, и так же для обоих вариантов цитаты, выноски, кода, тоггла, двух плотностей таблицы (`compact` / `comfortable`), каждой подписи к медиа (изображение, видео, аудио, файл, embed) и трёх частей закладки (заголовок, описание, ссылка). Опущенные ключи сохраняют встроенный размер Blok, поэтому везде, где вы не подключились явно, редактор отображается ровно как раньше. Настройки размера на уровне инструмента по-прежнему главнее: инструмент параграфа с `styles.size` или список с `itemSize` пишет этот размер инлайновым стилем на блоке, и его не переопределить никаким токеном — поэтому сценарии, которыми вы хотите управлять через `style.fontSize`, не должны одновременно нести размер на уровне инструмента. Значения могут быть абсолютными или относительными (`px`, `rem`, `em`, `%`): каждый декоративный элемент рядом с текстом заданного размера — маркер списка, чекбокс, эмодзи выноски, стрелка тоггла — выводит собственные метрики из того же токена, поэтому остаётся оптически выровненным при любом масштабе без дополнительного CSS. Так как эти токены читаются с запасными значениями и никогда не объявляются самим Blok, редактор, который НЕ настраивает `style.fontSize`, принимает их и из обычного CSS-правила на любом предке, и из `style.tokens` / `editor.tokens.set()`. Сами ИМЕНА токенов поставляются константой — `import { BLOK_FONT_SIZE_TOKENS } from '@dodopizza/blok'` даёт карту ровно той же формы, что и конфигурация (`BLOK_FONT_SIZE_TOKENS.paragraph`, `BLOK_FONT_SIZE_TOKENS.heading[1]`, `BLOK_FONT_SIZE_TOKENS.bookmark.link`…), поэтому хосту, который задаёт типографику из CSS, никогда не приходится переписывать эти строки вручную, а переименование становится ошибкой компиляции, а не молчаливым no-op. Каналы дополняют друг друга: `style.fontSize` — это значение на момент создания, а `editor.tokens.set({ [BLOK_FONT_SIZE_TOKENS.paragraph]: '18px' })` переопределяет его на лету — таблица токенов темы внедряется сразу после таблицы fontSize при равной специфичности, поэтому она выигрывает. Именно этот канал нужен размеру, который должен меняться после монтирования (переключатель плотности, масштаба или доступности); сам `style.fontSize` читается один раз при создании. В отличие от `style.tokens`, внедряемая таблица fontSize ограничена своим редактором: обёртка несёт `data-blok-instance`, и селектор редактора в этой таблице привязан к нему, поэтому второй редактор на странице сохраняет встроенные размеры Blok (или собственную конфигурацию), а не наследует размеры первого. Общестраничным остаётся лишь интерфейс, смонтированный в body, — поповеры и подсказки отрисовываются вне поддерева любого редактора, поэтому при расхождении экземпляров эти правила следуют порядку в `<head>`. Стоит знать одно правило вложенности: выноска отображает свой текст как дочерний блок-параграф, поэтому текст выноски следует `fontSize.callout` и откатывается к `fontSize.paragraph`, когда этот ключ не задан — задав только `paragraph`, вы измените размер тела выносок вместе с основным текстом, а чтобы они различались, нужно задать `fontSize.callout` явно. Наконец, view-рендерер (`@bloklabs/core/view`) выдаёт семантический HTML, и его таблица стилей несёт только сценарии, основанные на классах: в выводе view работают параграф, заголовки, список, чек-лист, оба размера цитаты, выноска, код и тоггл, а размеры подписей, ячеек таблицы и закладок доступны только в редакторе.

Свойства

СвойствоТипОписание
blockstringСтили базовой обёртки блока
inlineToolButtonstringСтили кнопки строчной панели
inlineToolButtonActivestringСтили активной кнопки строчного инструмента
inputstringСтили элемента ввода
loaderstringСтили индикатора загрузки
settingsButtonstringСтили кнопки настроек
settingsButtonActivestringСтили активной кнопки настроек
settingsButtonFocusedstringСтили кнопки настроек в фокусе
settingsButtonFocusedAnimatedstringСтили кнопки настроек в фокусе с анимацией нажатия
buttonstringСтили кнопки общего назначения
TypeScript
// Customize the editor from your host app via CSS custom properties —
// no need to target Blok's internal test IDs or data attributes.
// The hooks below are read with fallbacks and never declared by Blok, so
// they inherit from ANY ancestor — a plain container rule works:
.my-editor-container {
  /* Cap the content column at a custom width (default: 720px) */
  --blok-content-max-width: 650px;

  /* Extra start padding on list blocks (default: 0px) */
  --blok-list-padding-start: 18px;

  /* Checklists follow --blok-list-padding-start unless this is set —
     use it to indent checklists independently of other list styles */
  --blok-checklist-padding-start: 0px;

  /* Gap between a list marker/checkbox and its content (default: 0px) */
  --blok-list-gap: 6px;

  /* Indent applied per nesting level to blocks nested with Tab
     (default: 24px). Set it to 0px to switch nesting indentation off. */
  --blok-block-indent-step: 24px;

  /* Padding of every block tool wrapper. Fallbacks keep each tool's own
     default (7px/7px/2px for most blocks; quotes fall back to 0.2em
     vertical). Tighten all three for compact read-only inline
     rendering — no need to override [data-blok-tool]: */
  --blok-block-padding-top: 0;
  --blok-block-padding-bottom: 0.2em;
  --blok-block-padding-inline: 0;

  /* The callout card's own inset (default 5px) is deliberately separate
     from block rhythm — the rhythm override above leaves it alone: */
  --blok-callout-padding-block: 4px;

  /* Placeholder color of empty blocks (default: follows --blok-gray-text) */
  --blok-placeholder-color: rgba(112, 118, 132, 0.6);

  /* Heading typography (defaults mirror the built-in scale) */
  --blok-heading-1-font-size: 32px;
  --blok-heading-font-weight: 600;
  --blok-heading-margin-top: 16px;
  --blok-heading-margin-bottom: 16px;

  /* Space above embed blocks (default: 8px) */
  --blok-embed-margin-top: 16px;
}

// Container tools decline the nesting indent automatically: Blok zeroes the
// step inside every [data-blok-nested-blocks] child slot, so blocks your
// container positions itself are never also pushed by their depth. The indent
// is plain CSS, so opting it back IN inside your container is a declaration,
// not an !important fight:
.my-container-tool [data-blok-nested-blocks] {
  --blok-block-indent-step: 24px;
}

// Primary way to override theme tokens: style.tokens in the constructor
// config. Blok injects a per-instance stylesheet that reaches the editor
// AND UI portaled to document.body (popovers, tooltips, top-layer
// elements) automatically — no manual selector targeting needed.
new Blok({
  style: {
    tokens: {
      '--blok-selection': 'rgba(35, 131, 226, 0.28)',
      '--blok-popover-bg': '#1f1f1f',

      // Surface backgrounds. Most hover/light surfaces follow
      // --blok-bg-light; media empty-state cards use --blok-bg-secondary
      // with --blok-border-secondary; image/file loading skeletons and
      // upload placeholders use --blok-bg-tertiary, which defaults to
      // --blok-bg-light. Override the specific token you mean — no need
      // to overload --blok-bg-light to reach the skeleton surface.
      // NOTE: palette-backed color tokens like these are re-declared by
      // Blok on the editor wrapper itself, so set them here (or with a
      // CSS selector matching the wrapper, as below) — a declaration on
      // an ancestor container is shadowed and does nothing.
      '--blok-bg-light': '#eff2f5',
      '--blok-bg-secondary': '#f7f8fa',
      '--blok-border-secondary': 'rgba(55, 53, 47, 0.09)',
      '--blok-bg-tertiary': '#f0f0f0',
    },
  },
});

// CSS-only alternative: a plain host selector works too — Blok's palette
// is declared at zero specificity via :where(), so this always wins.
// Popovers/menus portal to document.body, so target them explicitly too.
[data-blok-interface],
[data-blok-popover],
[data-blok-top-layer] {
  --blok-popover-bg: #1a1a1a;

  /* Placeholder color of popover search inputs — wrapper-declared by Blok
     (like the palette) and consumed inside body-mounted popovers, so set
     it here or via style.tokens, never on an ancestor container: */
  --blok-search-input-placeholder: rgba(112, 118, 132, 0.8);
}

// Blok reserves 56px of start gutter automatically in edit mode for the
// floating +/⠿ block controls, collapsing to 0 only when the gutter is
// dead space: chromeless read-only (data-blok-controls-hidden) or
// hideToolbar (data-blok-toolbar-hidden). Plain read-only keeps the
// gutter so in-place mode flips never shift the layout.
// The gutter tokens are declared on the wrapper by Blok (default + state
// collapses), so overrides MUST target the wrapper itself — an ancestor
// container rule is shadowed and does nothing. Set any value, including
// 0px to remove the gutter, or redeclare it to opt back into the
// reserved space while controls are hidden:
.my-editor-container [data-blok-interface] {
  --blok-editor-gutter-start: 56px;
  --blok-editor-gutter-end: 16px;
}

// Center the content column instead of left-aligning it (default: 'left')
const editor = new Blok({
  holder: 'editor',
  style: { contentAlign: 'center' },
});

// Per-block, per-scenario type scale — every text-bearing block (and every
// scenario inside it: a caption, a density mode, a size variant) is settable
// from the config. This is the supported way to resize block text; there is
// no need to target Blok's internal class names. Each entry writes that
// block's --blok-*-font-size token. Omitted keys keep Blok's built-in size,
// and values may be absolute or relative (px / rem / em / %) — bullets,
// checkboxes, the callout emoji and the toggle arrow derive their metrics
// from the same token, so they stay aligned at any scale.
new Blok({
  holder: 'editor',
  style: {
    fontSize: {
      paragraph: '17px',
      // Headings reuse the existing --blok-heading-N-font-size tokens
      heading: { 1: '2.25rem', 2: '1.75rem', 3: '1.375rem' },
      list: { item: '17px', checklist: '17px' },
      quote: { default: '17px', large: '1.4em' },
      // Callout text is a child paragraph block: it follows this key, and
      // falls back to fontSize.paragraph when this key is omitted. Set it
      // explicitly to make callout text differ from body text.
      callout: '17px',
      code: '13px',
      toggle: '17px',
      table: { compact: '14px', comfortable: '17px' },
      image: { caption: '13px' },
      video: { caption: '13px' },
      audio: { caption: '13px' },
      file: { caption: '13px' },
      embed: { caption: '13px' },
      bookmark: { title: '15px', description: '13px', link: '13px' },
    },
  },
});

// Sizes that must CHANGE after mount (density / zoom / accessibility toggle)
// go through the token channel instead — style.fontSize is read once at
// construction and its stylesheet outranks tokens.set() for the scenarios it
// declares. So leave those scenarios out of style.fontSize entirely and
// size them from the token channel only:
editor.tokens.set({
  '--blok-paragraph-font-size': compact ? '15px' : '18px',
  '--blok-list-font-size': compact ? '15px' : '18px',
});

// Unconfigured scenarios also accept a plain CSS rule from any ancestor —
// Blok reads these tokens with fallbacks and never declares them itself:
.my-editor-container {
  --blok-quote-large-font-size: 24px;
}

// NOTE: the sheet Blok injects for style.fontSize is page-global, like
// style.tokens — it retypes EVERY editor on the page, including instances
// that set no fontSize of their own. Scope per-instance differences with a
// rule on that editor's own wrapper:
#editor-b [data-blok-interface] {
  --blok-paragraph-font-size: 15px;
}

// Flip theme tokens at runtime — e.g. from a host light/dark toggle.
// set() replaces the whole set, so tokens dropped from the new palette
// stop applying. Available immediately; calls before isReady are buffered.
editor.tokens.set({
  '--blok-popover-bg': isDark ? '#1f1f1f' : '#ffffff',
  '--blok-text-primary': isDark ? '#e6e6e6' : '#1a1a1a',
});
editor.tokens.get(); // -> currently applied tokens

// In React/Vue the same channel is a reactive prop (Angular: [styleTokens])
<BlokEditor style={{ tokens: isDark ? darkTokens : lightTokens }} />

// Opt out of Blok's ::selection repaint and use the native/host-defined
// selection colors instead (recoloring is possible via
// --blok-selection-inline; reverting to the UA default needs this flag)
new Blok({
  holder: 'editor',
  style: { nativeSelection: true },
});

// Access CSS class names for styling custom tools
const styles = editor.styles;

// Use class names in your custom tool
class MyCustomTool {
  constructor({ api }) {
    this.api = api;
  }

  render() {
    const wrapper = document.createElement('div');
    wrapper.className = this.api.styles.block;

    const input = document.createElement('input');
    input.className = this.api.styles.input;

    const button = document.createElement('button');
    button.className = this.api.styles.button;
    button.textContent = 'Click me';

    wrapper.appendChild(input);
    wrapper.appendChild(button);

    return wrapper;
  }
}

// Available class names:
// - api.styles.block              // Base block wrapper
// - api.styles.inlineToolButton   // Inline toolbar button
// - api.styles.inlineToolButtonActive  // Active inline tool
// - api.styles.input              // Input elements
// - api.styles.loader             // Loading spinner
// - api.styles.settingsButton     // Settings button
// - api.styles.settingsButtonActive   // Active settings
// - api.styles.settingsButtonFocused  // Focused settings
// - api.styles.settingsButtonFocusedAnimated  // Focused settings with click animation
// - api.styles.button             // General button