This document describes how to hide or disable parts of the editor UI. It is intended for developers who embed the Typograph editor and want to tailor its interface to their application.
// The API object is available as a global after the editor bundle loads.
const ui = window.gEditorUI;
Hide — the element is removed from view.
Disable — the element remains visible but is greyed out and cannot be
interacted with.
By default, hiding a top-level section removes it from the layout entirely
(display: none) so no empty space is left behind. This is called collapse.
For fields inside a section — for example, hiding just the X-position field
inside the size panel — the default is keepSpace (visibility: hidden),
which makes the field invisible while keeping its space so sibling fields stay
aligned.
You can override the default for any key:
ui.hide('parameterbar.position_size.position'); // keepSpace (default)
ui.hide('parameterbar.position_size.position', { collapse: true }); // collapse instead
ui.hide('parameterbar.shadow'); // collapse (default)
ui.hide('parameterbar.shadow', { collapse: false }); // keepSpace instead
.hide(key, options?)Hide a UI element.
ui.hide(key: string, options?: { collapse?: boolean }): EditorUI
.show(key)Show a previously hidden element.
ui.show(key: string): EditorUI
.disable(key)Make a UI element visible but non-interactive (greyed out).
ui.disable(key: string): EditorUI
.enable(key)Re-enable a previously disabled element.
ui.enable(key: string): EditorUI
.batch(fn)Apply multiple changes with a single UI update at the end.
Always use batch() when configuring more than one item at startup.
ui.batch(fn: () => void): void
All methods except batch() return this and can be chained.
.set_keywords(keywords)Provide a list of keywords that the editor user can insert into rich text at the cursor position.
ui.set_keywords(keywords: string[]): EditorUI
Keywords are sorted alphabetically and stored internally. Call with an empty array to clear all keywords.
When at least one keyword is set, pressing ⌘K while editing rich text opens a floating keyword popup near the cursor. The popup contains a search field that filters the list as the user types. Clicking a keyword inserts it at the cursor; the popup stays open so multiple keywords can be inserted in a row (press Esc or click outside to close it). When no keywords are set, ⌘K does nothing.
ui.set_keywords(['city', 'company', 'date', 'first_name', 'last_name']);
The call can be made at any time — before or after the editor is loaded, and can be updated at runtime (for example, when the user switches context and a different keyword set applies).
Controls how spreads are serialised when the user saves or exports. Two independent settings are provided — one for the Save command, one for the Save As / Export command:
ui.set_save_mode(mode: 'spreads' | 'single_pages' | 'single_pages_keep_together'): EditorUI
ui.get_save_mode(): 'spreads' | 'single_pages' | 'single_pages_keep_together'
ui.set_export_mode(mode: 'spreads' | 'single_pages' | 'single_pages_keep_together'): EditorUI
ui.get_export_mode(): 'spreads' | 'single_pages' | 'single_pages_keep_together'
| Mode | Behaviour |
|---|---|
'spreads' (default) |
Spreads are saved exactly as authored. |
'single_pages' |
Every spread is split into single pages in the output. |
'single_pages_keep_together' |
Spreads are split into single pages except those with "Keep pages together" on (islands) — those stay as multi-page spreads. |
Both settings default to 'spreads', so the default behaviour is unchanged.
The split happens only at serialisation time — the live document, its on-screen
spreads, and the undo history are not modified.
// Save keeps spreads; Export splits them into single pages
ui.set_save_mode('spreads');
ui.set_export_mode('single_pages');
// Split into single pages but keep "Keep pages together" spreads intact
ui.set_export_mode('single_pages_keep_together');
The call can be made at any time and updated at runtime.
Three modes: Editing, which is the editor as it has always been, Commenting, in which the canvas is inert and the user leaves anchored comments instead of changing the design, and Viewing, in which the document is read-only and there is no comment surface at all.
ui.set_available_modes(
modes: Array<'editing' | 'commenting' | 'viewing'>,
options?: { render?: boolean },
): EditorUI
ui.get_available_modes(): Array<'editing' | 'commenting' | 'viewing'>
ui.set_mode(mode: 'editing' | 'commenting' | 'viewing'): EditorUI
ui.get_mode(): 'editing' | 'commenting' | 'viewing'
The default is ['editing'], which draws no mode switcher and changes
nothing. A host that never calls set_available_modes gets exactly today's
editor.
| What the host offers | What the user sees |
|---|---|
['editing'] (default) |
No switcher. Today's editor. |
['editing', 'commenting'] |
A two-button switcher floating over the canvas, above the zoom control. |
['commenting'] |
A locked label reading "Commenting" in the same place, with a tooltip explaining the access. |
['viewing'] |
A locked label reading "View only". Inert canvas, no authoring bars, no comment rail. |
Pass { render: false } when the host draws its own control — it then owns the
chrome and calls set_mode() itself, while the widget still applies the mode.
portal-ui does this: mode is the same question as sharing, and it belongs in the
bar that answers it. With render left alone the widget draws the control
itself, floating over the bottom-right of the canvas above the zoom readout.
Give a commenter ['commenting'] alone rather than disabling the Editing
button: the locked label is what tells them on screen why the tools are gone,
and a silently inert toolbar reads as a bug.
set_mode is refused for a mode the host has not offered. If the current mode
is dropped from the available list, the first available mode is adopted.
Anything other than Editing calls typograph.set_interactive(false) and hides
the authoring controls: the toolbar and content-bar keys documented below, plus
the right-hand parameter bar and the page rail's add-page tile, which rebuild
themselves and are therefore hidden by container.
Two authoring routes need more than a muted canvas, because neither needs a selection:
MenuAPI stays hidden when Editing returns.draggable and reorder the
document when dropped. They are made undraggable for as long as the mode
lasts, and the rail's own rebuilds are watched so it stays that way.It is not a security boundary — it is a user interface. The host still enforces permission on its own server.
typograph_comments_toggledFires whenever the rail opens, closes, or its thread count changes — enough for a host to keep its own Comments button in step.
document.addEventListener('typograph_comments_toggled', (e) => {
e.detail.open; // is the rail on screen
e.detail.count; // threads it would list, under the current filter
e.detail.available; // false when there is no comment source, or the mode is 'viewing'
});
typograph_comment_pins_changedFires when the pin toggle flips, for a host drawing its own show-pins control.
document.addEventListener('typograph_comment_pins_changed', (e) => {
e.detail.visible; // whether pins are on screen in the current mode
});
typograph_mode_changeddocument.addEventListener('typograph_mode_changed', (e) => {
console.log(e.detail.mode); // 'editing' | 'commenting' | 'viewing'
});
The editor renders comments and reports what the user did. It performs no requests of its own — it holds no URL and no access token, and the host supplies every read and write. That keeps the bundle free of any one backend, so an embedder can wire it to their own.
ui.set_comment_source(source: comment_source): EditorUI
ui.set_comments_open(open: boolean): EditorUI
ui.get_comments_open(): boolean
ui.toggle_comments(): EditorUI
ui.set_current_user(user: { id: string, name: string, avatar_url?: string }): EditorUI
ui.refresh_comments(): EditorUI
ui.select_comment(id: string): EditorUI
ui.set_comment_pins_visible(visible: boolean): EditorUI
ui.get_comment_pins_visible(): boolean
ui.toggle_comment_pins(): EditorUI
Nothing is drawn until set_comment_source is called: no pins, and no rail.
The surface follows Google Drive's commenting, which is the interaction most reviewers already know:
+N. Hover spreads the faces, a click opens a picker
listing every thread in the group — the only way to reach the one underneath.
The cluster is computed on screen positions, so zooming in dissolves it.set_comment_pins_visible() is the same switch for a host. Commenting
always draws the pins and Viewing never does, so get_comment_pins_visible()
answers for the live mode rather than for the raw toggle.set_comments_open(true),
on clicking a pin, on starting a comment, and on a deep link; it closes with the
✕ in its header, with Escape, or on set_comments_open(false).Open / All in the header filters resolved threads.select_comment(id) given before the first list() resolves is remembered
and applied when the threads arrive, so a deep link does not race the network.Every method is async and may reject. The widget applies the change locally
first and rolls it back if the host rejects it, so a dropped pin appears under
the cursor rather than a third of a second later — and disappears again if the
write fails.
interface comment_source {
list: () => Promise<comment_thread[]>;
create: (comment: string, position: comment_position) => Promise<comment_thread>;
update: (id: string, comment: string) => Promise<comment_thread>;
resolve: (id: string, resolved: boolean) => Promise<void>;
remove: (id: string) => Promise<void>;
create_reply: (comment_id: string, comment: string) => Promise<comment_reply>;
update_reply: (comment_id: string, reply_id: string, comment: string) => Promise<comment_reply>;
remove_reply: (comment_id: string, reply_id: string) => Promise<void>;
}
interface comment_position {
page_uuid: string; // the Spread uuid the pin is anchored to
x: number; // native millimetres, not screen pixels
y: number;
}
interface comment_thread {
id: string;
user: { id: string, name: string, avatar_url?: string };
comment: string;
position: comment_position;
resolved: boolean;
created_at: string; // ISO 8601
updated_at: string;
replies: comment_reply[];
}
interface comment_reply {
id: string;
user: { id: string, name: string, avatar_url?: string };
comment: string;
created_at: string;
updated_at: string;
}
There is deliberately no list_replies: list returns each thread with its
replies already, and a second round trip per thread would buy nothing.
Positions are native millimetres, so a comment placed at one zoom level
lands in the same spot at another. page_uuid is the canvas Spread.uuid —
stable across page insert and reorder, where an index is not. A thread whose
page_uuid is an empty string is skipped rather than drawn on page one, which
is how comments predating anchoring are handled.
gEditorUI.set_current_user({ id: 'u-17', name: 'Robin de Vries' });
gEditorUI.set_comment_source({
list: () => fetch('/api/comments').then(r => r.json()),
create: (comment, position) => fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment, position }),
}).then(r => r.json()),
// …update, resolve, remove, create_reply, update_reply, remove_reply
});
gEditorUI.set_available_modes(['editing', 'commenting']);
select_comment(id) brings the thread's page into view and expands its card —
for landing a reader on the comment a notification named.
| Event | detail |
|---|---|
typograph_comment_created |
{ id } |
typograph_comment_updated |
{ id } |
typograph_comment_resolved |
{ id, resolved } |
typograph_comment_deleted |
{ id } |
typograph_comment_selected |
{ id } — id is null when the selection is cleared |
Parent keys target the whole group. Child keys (indented) target individual items within that group.
| Key | What it controls |
|---|---|
toolbar.select |
Selection tool |
toolbar.path |
Path / pen tool |
toolbar.grab |
Pan / grab tool |
toolbar.undo_redo |
Undo and Redo buttons (both together) |
toolbar.undo_redo.undo |
Undo button only |
toolbar.undo_redo.redo |
Redo button only |
toolbar.fill_color |
Fill colour swatch |
toolbar.stroke |
Stroke palette — icon trigger; opens a popup |
toolbar.stroke.stroke_size |
Stroke width input |
toolbar.stroke.stroke_pattern |
Stroke dash pattern selector |
toolbar.stroke.line_cap |
Line cap buttons |
toolbar.stroke.line_join |
Line join buttons |
toolbar.opacity_blend |
Opacity + blend palette — icon trigger; opens a popup |
toolbar.opacity_blend.opacity |
Opacity control |
toolbar.opacity_blend.blend_mode |
Blend mode selector |
toolbar.text_format |
All text formatting controls |
toolbar.font_face |
Font family selector |
toolbar.font_face.default_font |
Default font (Source Sans Pro) entry in the font picker |
toolbar.font_face.manage_fonts |
"Manage fonts…" item at the top of the font picker |
toolbar.font_style |
Font style selector |
toolbar.font_size |
Font size input |
toolbar.text_align |
Horizontal justification (icon popupmenu) |
toolbar.text_valign |
Vertical alignment (icon popupmenu) |
toolbar.decoration |
Underline / strikethrough palette — icon trigger; opens a popup |
toolbar.decoration.underline |
Underline switch + weight / offset / colour |
toolbar.decoration.strikethrough |
Strikethrough switch + weight / offset / colour |
toolbar.spacing |
Spacing palette — icon trigger; opens a popup |
toolbar.spacing.line_spacing |
Line spacing input |
toolbar.spacing.letter_spacing |
Letter spacing input |
toolbar.columns |
Columns palette — icon trigger; opens a popup |
toolbar.columns.columns_count |
Column count icon + input |
toolbar.columns.columns_gutter |
Gutter icon + input |
toolbar.columns.columns_balanced |
Balanced / unbalanced toggle buttons |
toolbar.drop_cap |
Drop-cap palette — icon trigger; opens a popup |
toolbar.drop_cap.drop_cap_lines |
Drop cap label + lines input |
toolbar.drop_cap.drop_cap_chars |
Chars label + characters input |
toolbar.drop_cap.drop_cap_size |
Size label + font size input |
toolbar.hyphenation |
Hyphenation palette — icon trigger; opens a popup |
toolbar.hyphenation.hyphenation_toggle |
Hyphenate label + on/off switch |
toolbar.hyphenation.hyphenation_before |
Before label + min-chars-before-break input |
toolbar.hyphenation.hyphenation_after |
After label + min-chars-after-break input |
toolbar.hyphenation.hyphenation_limit |
Limit label + max-consecutive-hyphenated-lines input (0 = unlimited) |
toolbar.spellcheck |
Spell-check palette — icon trigger; opens a popup |
toolbar.spellcheck.spellcheck_toggle |
Spell check label + on/off switch |
toolbar.image_controls |
Image scale, horizontal and vertical alignment |
Popup palettes: several groups (
toolbar.stroke,toolbar.opacity_blend,toolbar.decoration,toolbar.spacing,toolbar.columns,toolbar.drop_cap,toolbar.hyphenation,toolbar.spellcheck) render as an icon-only trigger that opens a flyout panel. Hiding/disabling the palette key affects the trigger; the indented…<control>keys target individual controls inside the popup and are applied each time the popup opens.Text-edit vs frame toolbar: when editing text (double-click), the toolbar shows stroke + opacity/blend palettes, the justification popupmenu, and the decoration / spacing palettes. When a text frame is selected (not editing), it shows the vertical-alignment popupmenu and the columns, drop-cap, hyphenation and spell-check palettes.
toolbar.text_alignandtoolbar.text_valignare now single icon popupmenus (no per-direction sub-keys).
| Key | Tab |
|---|---|
contentbar.pages |
Pages / templates |
contentbar.text |
Text snippets |
contentbar.shapes |
Shapes |
contentbar.images |
Images |
contentbar.clips |
Clips |
contentbar.layers |
Layers |
Shown when the user clicks the canvas background.
| Key | Section |
|---|---|
parameterbar.page.name |
Page name |
parameterbar.page.size |
Page width and height (both together) |
parameterbar.page.width |
Page width field only |
parameterbar.page.height |
Page height field only |
parameterbar.page.type |
Page type selector |
parameterbar.page.fill_color |
Page background colour |
parameterbar.page.bleed |
Entire bleed section |
parameterbar.page.locked |
Page locked toggle |
Individual bleed fields — default to keepSpace
| Key | Field |
|---|---|
parameterbar.page.bleed.top |
Bleed top |
parameterbar.page.bleed.right |
Bleed right |
parameterbar.page.bleed.bottom |
Bleed bottom |
parameterbar.page.bleed.left |
Bleed left |
parameterbar.page.bleed.link |
Link / unlink all values button |
Sections
| Key | Section |
|---|---|
parameterbar.position_size |
Entire name / position / size panel |
parameterbar.rotate |
Rotation |
parameterbar.lock |
Lock |
parameterbar.corners |
Corner radius |
parameterbar.shadow |
Shadow |
parameterbar.textwrap |
Text-wrap section (entire panel) |
Fields within the size / position section — default to keepSpace
| Key | Field(s) |
|---|---|
parameterbar.position_size.name |
Element name input |
parameterbar.position_size.position |
X and Y position |
parameterbar.position_size.size |
Width and Height |
parameterbar.position_size.reference |
9-point reference selector |
Text-wrap sub-controls — default to collapse
Shown on the obstructing element (a shape/image that text flows around). The offset, side and inverse controls only appear once a wrap mode other than none is chosen.
| Key | Control |
|---|---|
parameterbar.textwrap.mode |
The four wrap-mode icons (none / bounding box / jump / shape) |
parameterbar.textwrap.offset |
Standoff offset input |
parameterbar.textwrap.side |
Side selector (both / left / right / largest area) |
parameterbar.textwrap.inverse |
Inverse toggle (flow text inside the shape) |
ui.batch(() => {
ui.hide('toolbar.path');
ui.hide('toolbar.grab');
});
ui.batch(() => {
ui.hide('contentbar.pages');
ui.hide('contentbar.text');
ui.hide('contentbar.shapes');
ui.hide('contentbar.clips');
});
ui.batch(() => {
ui.hide('parameterbar.shadow');
ui.hide('parameterbar.rotate');
ui.hide('parameterbar.corners');
});
ui.hide('parameterbar.position_size.name');
// X, Y, Width, Height fields remain visible and aligned
ui.disable('toolbar.blend_mode');
ui.disable('parameterbar.rotate');
document.addEventListener('typograph_setup_ready', () => {
ui.batch(() => {
ui.hide('toolbar.path');
ui.hide('toolbar.grab');
ui.hide('toolbar.blend_mode');
ui.hide('toolbar.text_baseline');
ui.hide('contentbar.pages');
ui.hide('contentbar.text');
ui.hide('contentbar.shapes');
ui.hide('contentbar.clips');
ui.hide('parameterbar.shadow');
ui.hide('parameterbar.rotate');
ui.hide('parameterbar.position_size.name');
});
});
// Hide initially, reveal later based on application state
ui.hide('toolbar.drop_cap');
// ... later ...
ui.show('toolbar.drop_cap');
ui.set_keywords(['city', 'company', 'date', 'first_name', 'last_name']);
// Press ⌘K while editing text to open the keyword popup
// Different keyword sets for different document types
function on_document_type_changed(type: string) {
if (type === 'invoice')
ui.set_keywords(['company', 'invoice_date', 'invoice_number', 'total']);
else if (type === 'letter')
ui.set_keywords(['city', 'date', 'first_name', 'last_name', 'salutation']);
else
ui.set_keywords([]); // disable keyword popup
}