Skip to content

The Definitions Format

Everything a block is lives in one JSON document. A complete, small example:

{
"elementTypes": {
"icon": "image",
"title": "text",
"description":"text",
"concept": "code",
"python": "code",
"javascript": "code"
},
"code": {
"python": {
"stringQuote": "\"",
"empty": {
"Number": { "shadow": "math_number", "fieldValues": { "NUM": "42" } },
"String": { "shadow": "text", "fieldValues": { "TEXT": "world" } }
},
"highlighting": { "keywords": ["print", "if", "for", "in"], "strings": ["\"", "'"], "comment": "#" }
},
"javascript": {
"stringQuote": "\"",
"empty": {
"Number": { "shadow": "math_number", "fieldValues": { "NUM": "42" } },
"String": { "shadow": "text", "fieldValues": { "TEXT": "world" } }
},
"highlighting": { "keywords": ["console", "if", "for", "let"], "strings": ["\"", "'"], "comment": "//" }
}
},
"modes": [
{ "name": "icon-tiles", "elements": ["icon", "title", "description"] },
{ "name": "concept-tiles", "elements": ["title", "concept"] },
{ "name": "python-text-tiles", "elements": ["title", "python"], "render": { "python": "text-based" } }
],
"views": {
"editor-pane": "workspace",
"text-pane": "codespace",
"read-pane": { "kind": "codespace", "editable": false }
},
"presets": [
{ "name": "icon-preset", "label": "Iconic", "toolbox": "icon-tiles", "views": { "editor-pane": "concept" } },
{ "name": "hybrid-preset", "label": "Hybrid", "toolbox": "concept-tiles",
"views": { "editor-pane": "concept", "text-pane": "python", "read-pane": "javascript" } }
],
"categories": [
{ "name": "Output", "color": "#5C81A6" }
],
"blocks": [
{
"identifier": "text_print",
"category": "Output",
"elements": {
"title": "Print",
"description":"Prints a value to the console",
"concept": "Output %1",
"python": "print(%1)",
"javascript": "console.log(%1);"
},
"inputSlots": {
"1": { "kind": "value", "name": "TEXT" }
}
}
]
}

Point your file at the JSON Schema shipped with the package and editors (VS Code, …) give autocomplete and inline validation as you type — the author-time counterpart to the mount-time validation:

{
"$schema": "./node_modules/morphic-blocks/definitions.schema.json",
"version": 1,
"blocks": [ /* … */ ]
}

$schema may be a relative path (to morphic-blocks/definitions.schema.json) or a URL. version marks the format revision (currently 1). Both are optional and ignored by the framework at runtime.

Global registry mapping element names to their type. A value is either a bare type string — "text" | "code" | "image" — or, for an image element that needs a size, a config object:

Field Applies to Purpose
type all "text", "code", or "image"
size image Display size when the value is a file path auto-wrapped as <img>: a number (32 → 32×32), "32", or "32x32". Defaults to 16×16.

See Blocks & Elements for what each type means.

How each code element’s language is written, keyed by element name (a mode’s source element already names the “language”). Every field is optional:

Field Purpose
stringQuote Delimiter wrapped around framework-supplied literals in String-checked slots, so the codespace renders print("hello") rather than print(hello). Omit to disable quoting.
empty Defaults for empty value slots (see below)
emptyStatement Written into an empty statement slot where the language needs one, e.g. "pass" for Python, so an empty loop body stays valid code. Omit to leave empty bodies empty.
types Names %1:type writes, keyed by the value’s type (see Type names)
highlighting Token colors for the codespaces (see below)
values How printed values read while this element is shown (True, None, ['a']); see Behaviors & Code Generation

Up to 0.2, stringQuote and empty sat on the element in elementTypes and highlighting was its own top level map. mount() reports those places with the new one.

What should a value slot show when nothing is attached? An empty-default config answers that, and appears in two places with the same shape:

  • code.<name>.empty — per element (per “language”), keyed by the slot’s check ("Number", "String", "Boolean", …). A default key acts as a catch-all, used when the slot’s check isn’t listed or the slot has no check at all.
  • inputSlots.<n>.default — per block slot; highest priority, beats the element’s lookup

The resolution order is inputSlots.<n>.default → empty[<check>] → empty.default.

"inputSlots": {
"1": {
"kind": "value", "name": "TEXT", "check": "String",
"default": { "shadow": "text", "fieldValues": { "TEXT": "Hello, world!" } }
}
}
Field Purpose
shadow Blockly block type used as a shadow — ghosted, immutable, auto-replaced when a real block connects, restored when it disconnects. E.g. "math_number", "text", "logic_boolean".
placeholder Blockly block type attached as a real block on render — movable, editable, deletable. When both are set, the placeholder takes priority on the visible slot; Blockly’s native shadow restoration brings the shadow back if the user removes the placeholder.
fieldValues Initial values for the chosen block’s fields, e.g. { "NUM": "42" } for math_number or { "TEXT": "hello" } for text.

The shadow / placeholder values resolve against your block identifiers first, then Blockly stock types — see Block identifiers. A shadow’s output type must be compatible with the slot’s check, otherwise Blockly silently rejects it.

A shadow default fills the slot in both views: the workspace shows the ghosted block and the codespace shows its value, editable in place (so a print with a String shadow renders print("hello") — text stays syntactically valid). A placeholder seats a real block that, once deleted, leaves the slot truly empty: it is attached on the first render that sees the slot and is never re-created, so a deletion survives mode switches and later renders.

When the shadow is one of your own blocks, the codespace writes it through that block’s template, so it reads exactly like the block placed there: a text block with the template "%TEXT" shows "hello", and a block without fields (a * for every column, say) still shows its text. A Blockly stock shadow such as text has no template, so its value is written as is, quoted by stringQuote.

Shadow values are applied once. A shadow is declared on a connection only when that connection has none, so the element rendered first supplies the value. Giving the same check different fieldValues per element — python 42, go 100 — therefore does not produce per-language defaults: whichever mode rendered first wins for all of them. Keep the values identical across elements and let per-language display come from a dropdown’s display map and stringQuote.

For a truly empty slot — a deleted placeholder, or no default at all — the workspace shows an empty socket, and the codespace (which can’t render “nothing”) shows a bracketed type marker derived from the slot’s check: [NUMBER], [TEXT], [BOOL], or [VALUE] when the slot has no check. Fill it by dragging a value block into the slot.

The list of mode definitions ({ name, elements, render? }), what toolbox tiles show; covered in detail in Modes.

The views by name, each a kind (workspace, codespace) or { kind, editable?, rtl?, delete? }; mount() sets up each one it gets a container for. Covered in Presets & Views.

What the toolbox and each view show ({ name, label?, toolbox, views }); covered in Presets & Views.

Optional toolbox groupings. Blocks reference them by name:

"categories": [{ "name": "Output", "color": "#5C81A6" }]

Syntax highlighting for the codespaces, set per code element as code.<name>.highlighting:

Field Meaning
keywords Words highlighted as keywords, as whole words in any script
strings String delimiters, e.g. ["\"", "'"], or [open, close] pairs like ["„", "“"]
comment Line-comment marker, e.g. "#" or "//"
numbers Highlight numeric literals (default true)
colors Optional per-token-class color overrides (keyword, string, number, comment)

No grammar files, no language plugins — see the Syntax Highlighting guide.

A flat array of block definitions:

Field Purpose
identifier Free-form block id (see namespacing)
category Optional category name
elements The name: content map of visual parts; optional default template (below)
inputSlots Configuration of %N slots (below)
fields Inline field widgets — dropdown/text/number/checkbox (below)
output Value-block output type (e.g. "Number")
shape How a statement block connects, in one word (below)
previousStatement / nextStatement Statement connections
color Block color (can also come from CSS)
tooltip, helpUrl, inputsInline Passed through to Blockly

Many templates are the same in every language. Instead of repeating them, write them once under the reserved key default:

"elements": {
"title": "Math",
"default": "%1 %OP %2"
}

default is used for every code element the block does not list itself. A listed element always wins, so you can override single languages:

"elements": {
"concept": "Variable %VAR",
"default": "%VAR"
}

Here concept shows Variable x, while python, javascript and every other code element show x.

  • It is optional. Writing every language out works exactly as before.
  • It only fills code elements. Text and image elements such as title or icon are never filled from it.
  • default is reserved, so it cannot be the name of an element in elementTypes.

shape says in one word how a statement block connects above and below:

shape Above Below Example
"statement" yes yes print, if
"start" no yes “when program starts”
"end" yes no return, stop
"standalone" no no a block that stays on its own
{ "identifier": "text_print", "shape": "statement", "...": "..." }

shape is about connecting, not about looks. How a block looks comes from modes and CSS.

  • It is optional. Without it, previousStatement and nextStatement decide, exactly as before, and a block with neither is standalone.
  • It only fills the flags the block does not set itself, so a flag you write wins. That is how you add a connection type: "shape": "statement", "previousStatement": "Action".
  • Value blocks don’t take a shape. They use output: true for any value, or a type such as "Number".
  • A flag that contradicts the shape, like "shape": "end" with "nextStatement": true, gives a validation warning.

inputSlots configures each %N placeholder, keyed by its number:

"inputSlots": {
"1": { "kind": "value", "name": "TEXT", "check": "String" }
}
Field Purpose
kind "value" (expression input) or "statement" (nested body)
name Blockly input name — behaviors read attached code via this name
check Type check ("Number", "String", …), also keys empty defaults
label Optional label text
align Input alignment
default Per-slot shadow/placeholder config — highest priority, beats the element’s code.<element>.empty lookup (see Shadows, placeholders, and empty slots)

check decides which blocks fit a value slot, in the workspace and when dropping into the codespace: a block fits when its output type matches. It also picks the slot’s empty default and its [TYPE] marker.

  • Set it when the program needs a certain kind of value: the numbers of a math block, the count of a repeat, the condition of a while that should only take comparisons.
  • Leave it out when any value works: the value of a print, or an if in a language where every value counts as true or false (Python, JavaScript). A slot without check accepts every block with an output.
"inputSlots": {
"1": { "kind": "value", "name": "CONDITION" },
"2": { "kind": "statement", "name": "THEN" }
}

This if takes a comparison, a number or a variable. With "check": "Boolean" on slot 1, a number would no longer fit, only Boolean blocks (true, false, comparisons). A block with "output": true has no type and fits any slot, with or without check.

Where inputSlots configures the %N sockets other blocks plug into, fields declares the inline widgets that live on the block itself — dropdowns, text boxes, number spinners, checkboxes — keyed by their %FIELDNAME token:

"elements": { "python": "%1 %OP %2" },
"fields": {
"OP": {
"type": "dropdown",
"options": ["+", ["-", "−"], ["*", "×"], ["/", "÷"]],
"default": "+"
}
}
Type Extra config
dropdown options (below), default (selected value)
text default
number default, min, max, precision
checkbox default (boolean)

A dropdown option is one of:

Form Meaning
"==" value = label = "=="
["-", "−"] [value, label] — value -, shown as −
{ "value": "-", "label": "−" } object form

The value is the source of truth: it is what the block generates, serializes, and executes. The optional label is a display-only override shown on the workspace block, so one option can show ÷ while the text views and generated code use /. There is no separate serialization key — Blockly stores the value.

A label can also be a picture. The block shows it in place of text; the text views and the generated code still use the value.

"fields": {
"DIR": {
"type": "dropdown",
"options": [
{ "value": "left", "label": { "src": "icons/arrow_left.svg" } },
{ "value": "right", "label": { "src": "icons/arrow_right.svg", "alt": "to the right" } }
]
},
"COLOR": {
"type": "dropdown",
"options": [
{ "value": "red", "label": { "color": "#e53935" } },
{ "value": "blue", "label": { "color": "#1e88e5" } }
]
}
}
Key Meaning
src An image file (SVG, PNG, …), like the image elements
color A CSS color; the framework draws a swatch, no file needed
alt What screen readers say. Default: the value
width, height Size in pixels. Default: 16 × 16

A picture needs src or color. Pictures suit choices that are easier to see than to read: colors, directions, animals, sprites, notes.

The value stays a word of your choice (red, ink), so the text views write it and display works. For picking any color as a hex code, use Blockly’s @blockly/field-colour plugin in a behavior’s onViewApplied.

An open dropdown’s menu gets two classes: morphic-menu, and morphic-menu-<FIELD> with the field’s name. The look is yours: lay pictures out in a row or grid, hide the check marks, make choices bigger for touch, or give an operator menu a larger font. For example, a pen color menu with the swatches in a row and an outline on the chosen one:

.morphic-menu-COLOR { display: flex; gap: 4px; padding: 4px; }
.morphic-menu-COLOR .blocklyMenuItem { min-width: 0; padding: 4px; }
.morphic-menu-COLOR .blocklyMenuItemCheckbox { display: none; }
.morphic-menu-COLOR img { display: block; width: 24px; height: 24px; border-radius: 6px;
outline: 3px solid transparent; outline-offset: 1px; }
.morphic-menu-COLOR .blocklyMenuItemSelected img { outline-color: #55bdcb; }

The field on the block keeps Blockly’s look. An app that wants it painted in the chosen color, as the Turtle Drawing app in the playground does, builds its own field in a behavior’s onViewApplied.

display makes an option’s shown text follow the active mode while the value stays single — the same mode-awareness the element system gives content, now for fields. It maps an element name (keyed like the code section) to the text shown when that element renders, so Python source reads True while JavaScript reads true, both storing and executing true:

"fields": {
"BOOL": {
"type": "dropdown",
"options": [
{ "value": "true", "display": { "python": "True" } },
{ "value": "false", "display": { "python": "False" } }
],
"default": "true"
}
}

Resolution when element E renders: the workspace block shows display[E] ?? label ?? value; the codespaces show display[E] ?? value (label stays block-only). Execution, codegen, and serialization always use the value. This is the representation axis only (Python vs JavaScript spelling) — natural-language translation is a separate concern. Only dropdown fields take display; text/number hold user data or language-neutral values.

Fields outside these four types (variables, colour, plugin/custom) are attached by a behavior’s onViewApplied instead — an undeclared %FIELDNAME token is left for the behavior to fill.

code element content is a template:

Syntax Result
%1, %2 Input slot — a Blockly input and a substitution point in text views
%FIELDNAME Inline field declared in fields (uppercase token, e.g. %NUM) — rendered on the block, substituted in text views
%1:type The name of the type of slot 1’s value (see Type names)
<img …> Image (Blockly FieldImage on workspace blocks)
Plain text Becomes a Blockly label field

Whitespace is preserved as authored in text renderings, and indentation compounds across nesting:

"python": "if %1:\n %2"

renders multi-line with its body indented — nested blocks indent further automatically.

%1:type writes the type of the value in slot 1, so a typed language needs no type dropdown and the type always fits the value. The value’s type is its block’s output check, or the slot’s check while the slot is empty. Each language names the types in its code settings; default is used when no key fits, and without it nothing is written:

"code": {
"java": { "types": { "Number": "int", "String": "String", "Boolean": "boolean", "default": "var" } }
},
"blocks": [{
"identifier": "declare",
"elements": { "java": "%1:type %NAME = %1;", "js": "let %NAME = %1;" },
"inputSlots": { "1": { "kind": "value", "name": "VALUE" } }
}]

With "hey" in the slot, Java reads String s = "hey";; with 5, int s = 5;. A language without types leaves the token out, as js does here. In blocks the name is a label that changes with the value. %2:type is the type of slot 2.

Take a block’s type from one place: %1:type in every language that writes a type, or a field of its own (e.g. int x; with no value), not both. A field in one language and %1:type in another can disagree.

The framework checks your definitions when you mount(), so problems that would otherwise fail silently at render time surface as clear messages naming the block. It throws on structural breakage that guarantees wrong output, and warns on config that is merely degraded or dead.

Every check, with an example message and how to fix it, is listed on Errors & Warnings.

To check a file before mounting — in a test or build step — call the exported validateDefinitions(...), which returns { errors, warnings } instead of throwing.

Warnings go to the browser console. Pass onWarning to mount() to receive them yourself, together with the framework’s other warnings (modes without CSS, missing Blockly media), for example to show them only while developing:

engine.mount({
workspaceContainer,
onWarning(message) {
if (import.meta.env.DEV) showDevBanner(message);
},
});