Skip to content

Blocks & Elements

The fundamental unit of the framework is the Morphic Block. It represents one program construct (a print statement, a loop, a condition, …) and carries a set of named elements — the visual parts it can show. Which parts it does show is decided later, by modes.

Elements are declared per block as a plain name: content map:

{
"identifier": "text_print",
"elements": {
"title": "Print",
"description":"Prints a value to the console",
"concept": "Output %1",
"python": "print(%1)",
"javascript": "console.log(%1);"
}
}

Element names are free-form — title, icon, concept, python, javascript are conventions, not keywords. You invent the vocabulary that fits your use case (natural languages, programming languages, verbosity levels, …).

What an element name means is declared once, globally, in the elementTypes registry — not repeated per block:

{
"elementTypes": {
"icon": "image",
"title": "text",
"description":"text",
"concept": "code",
"python": "code"
}
}

The type drives rendering behavior:

Type Toolbox tile Workspace block
text Rendered as HTML label Never shown
code Rendered as Blockly SVG or source text Used as the Blockly template (<img> in content becomes a FieldImage)
image Rendered as <img> Never shown
custom Drawn by a function of the app Never shown

code elements use the template syntax (%1 input slots, %FIELDNAME fields, <img> tags) and double as the text rendering for the codespaces.

A custom element is drawn on the tiles by a function of the app, so a tile can hold anything the three other types do not: an info button, a sound to play, a read aloud button, a video. The block’s content for the element is passed to the function, so each block has its own:

{
"elementTypes": { "title": "text", "concept": "code", "info": "custom" },
"modes": [
{ "name": "novice-tiles", "elements": ["title", "concept", "info"] },
{ "name": "expert-tiles", "elements": ["concept"] }
],
"blocks": [
{ "identifier": "print", "elements": {
"title": "Print", "concept": "print %1", "info": "Writes a message to the output" } }
]
}
engine.mount({
workspaceContainer,
toolboxContainer,
toolbox: {
custom: {
info: (content, { block, element, mode }) => {
const button = document.createElement("button");
button.textContent = "ⓘ";
button.setAttribute("aria-label", content);
button.onclick = () => showHelp(content);
return button;
},
},
},
});

The function returns a DOM node, or a string shown as text. It runs again on every redraw of the toolbox, e.g. when the toolbox mode switches. Like every element, modes decide where it shows: here the novice tiles have the button, the expert tiles do not.

A press on a button, link or input inside a custom element is for that control: it does not drag the tile, and Enter or Space there does not add the block. A custom element is left out of the tile’s name for screen readers, since its controls carry their own. Without a function in toolbox.custom, the tile shows the content as text and mount() warns.

In the HTML toolbox, all elements are always rendered; the active mode’s CSS controls which are visible:

<div class="morphic-block morphic-mode-icon-tiles morphic-block-text_print"
style="--morphic-block-color: #5C81A6">
<div class="morphic-element-icon"><img src="assets/log.svg"></div>
<div class="morphic-element-concept">Output <span class="morphic-slot"></span></div>
<div class="morphic-element-python">print(...)</div>
<div class="morphic-element-title">Print</div>
</div>

This is what your per-mode CSS targets — see Styling Modes.

Block identifiers are free-form too — including names that collide with Blockly’s built-ins like math_number or logic_boolean. Internally the framework registers every block as morphic:<identifier>, so your names can never clobber the stock blocks (which the framework still uses for shadows, placeholders, and connection checks).

You always work with the clean identifier: it keys the behaviors map, shadow and placeholder references, and toolbox block lists. A reference resolves to your block when it matches one of your definitions, and to the Blockly stock type otherwise — "shadow": "math_number" keeps Blockly’s number block, while "shadow": "my_number" uses yours if you defined it.

A live Blockly block still carries the namespaced type in block.type. To look up its definition, turn it back into the clean identifier with toCleanId:

import { toCleanId } from "morphic-blocks";
const id = toCleanId(block.type); // "morphic:text_print" → "text_print"

Types without the prefix, such as Blockly’s math_number, come back unchanged.