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" } } } ]}$schema and version
Section titled “$schema and version”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.
elementTypes
Section titled “elementTypes”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.
Shadows, placeholders, and empty slots
Section titled “Shadows, placeholders, and empty slots”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’scheck("Number","String","Boolean", …). Adefaultkey acts as a catch-all, used when the slot’s check isn’t listed or the slot has nocheckat 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.
presets
Section titled “presets”What the toolbox and each view show ({ name, label?, toolbox, views });
covered in Presets & Views.
categories
Section titled “categories”Optional toolbox groupings. Blocks reference them by name:
"categories": [{ "name": "Output", "color": "#5C81A6" }]highlighting
Section titled “highlighting”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.
blocks
Section titled “blocks”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 |
Default template
Section titled “Default template”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
codeelements. Text and image elements such astitleoriconare never filled from it. defaultis reserved, so it cannot be the name of an element inelementTypes.
Block shape
Section titled “Block shape”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,
previousStatementandnextStatementdecide, 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:truefor any value, or a type such as"Number". - A flag that contradicts the shape, like
"shape": "end"with"nextStatement": true, gives a validation warning.
Input slots
Section titled “Input slots”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) |
When a slot needs check
Section titled “When a slot needs check”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
whilethat should only take comparisons. - Leave it out when any value works: the value of a print, or an
ifin a language where every value counts as true or false (Python, JavaScript). A slot withoutcheckaccepts 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.
Fields
Section titled “Fields”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.
Pictures as choices
Section titled “Pictures as choices”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.
Styling the menu
Section titled “Styling the menu”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.
Per-mode option text
Section titled “Per-mode option text”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.
Template syntax
Section titled “Template syntax”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.
Type names
Section titled “Type names”%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.
Validation
Section titled “Validation”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); },});