Block Registration API
This guide explains how to add new blocks to Scratch's built-in block palette.
Overview
Adding new blocks to Scratch involves several components working together:
- Block Definitions (scratch-blocks) - Define the visual appearance and structure
- Block Implementation (scratch-vm) - Define the runtime behavior
- Compiler Support (scratch-vm) - Support for compiled mode
- Palette Registration (scratch-gui) - Make blocks visible in the toolbox
- Localization (scratch-blocks) - Text strings for different languages
Step 1: Define Block Structure (scratch-blocks)
Block definitions determine how blocks look and behave in the editor. They need to be added to both vertical and horizontal layout files.
Vertical Layout
File: scratch-blocks/blocks_vertical/control.js
Blockly.Blocks['control_switch'] = {
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROL_SWITCH,
"message1": "%1", // Statement
"args0": [
{
"type": "input_value",
"name": "VALUE"
}
],
"args1": [
{
"type": "input_statement",
"name": "SUBSTACK"
}
],
"category": Blockly.Categories.control,
"extensions": ["colours_control", "shape_statement"]
});
}
};
Horizontal Layout
File: scratch-blocks/blocks_horizontal/control.js
The horizontal layout includes additional styling for icons and layout:
Blockly.Blocks['control_switch'] = {
init: function() {
this.jsonInit({
"message0": Blockly.Msg.CONTROL_SWITCH,
"message1": "%1",
"message2": "%1", // Icon
"lastDummyAlign2": "RIGHT",
"args0": [
{
"type": "input_value",
"name": "VALUE"
}
],
"args1": [
{
"type": "input_statement",
"name": "SUBSTACK"
}
],
"args2": [
{
"type": "field_image",
"src": Blockly.mainWorkspace.options.pathToMedia + "repeat.svg",
"width": 24,
"height": 24,
"alt": "*",
"flip_rtl": true
}
],
"category": Blockly.Categories.control,
"extensions": ["colours_control", "shape_statement"]
});
}
};
Step 2: Implement Block Behavior (scratch-vm)
Block runtime behavior is implemented in scratch-vm's block packages.
Register Block Primitives
File: scratch-vm/src/blocks/scratch3_control.js
getPrimitives() {
return {
// ... existing blocks
control_switch: this.switch,
control_case: this.case,
control_default: this.default,
control_break: this.break
};
}
Implement Block Methods
switch(args, util) {
// Get the switch value
const switchValue = args.VALUE;
// Store in stack frame for case blocks to access
if (!util.stackFrame.switchValue) {
util.stackFrame.switchValue = switchValue;
util.stackFrame.switchMatched = false;
util.stackFrame.isBreakable = true;
}
// Execute the substack containing case blocks
util.startBranch(1, false);
}
case(args, util) {
const caseValue = args.VALUE;
const stackFrame = util.stackFrame;
// Find the parent switch frame
const parentFrame = this.getParentSwitchFrame(util.thread);
if (!parentFrame) return;
// Check if this case matches the switch value
if (parentFrame.switchValue === caseValue || parentFrame.switchMatched) {
parentFrame.switchMatched = true;
util.startBranch(1, false);
}
}
break(args, util) {
// Find the nearest breakable frame and exit
const thread = util.thread;
for (let i = thread.stackFrames.length - 1; i >= 0; i--) {
const frame = thread.stackFrames[i];
if (frame.isBreakable || frame.isLoop) {
// Clear the stack back to this frame
thread.stackFrames.length = i;
return;
}
}
}
Step 3: Add Compiler Support
For blocks to work in compiled mode, add support to the compiler's intermediate representation (IR) and JavaScript generators.
IR Generation
File: scratch-vm/src/compiler/irgen.js
case 'control_switch':
return {
kind: 'control.switch',
value: this.descendInput(block, 'VALUE'),
stack: this.descendSubstack(block, 'SUBSTACK')
};
case 'control_case':
return {
kind: 'control.case',
value: this.descendInput(block, 'VALUE'),
stack: this.descendSubstack(block, 'SUBSTACK')
};
case 'control_break':
return {
kind: 'control.break'
};
JavaScript Generation
File: scratch-vm/src/compiler/jsgen.js
case 'control.switch': {
const switchValue = this.localVariables.next();
this.source += `var ${switchValue} = ${this.descendInput(node.value).asUnknown()};\n`;
this.source += `switch (${switchValue}) {\n`;
const switchFrame = new Frame(false);
switchFrame.isBreakable = true;
this.descendStack(node.stack, switchFrame);
this.source += `}\n`;
break;
}
case 'control.case': {
this.source += `case ${this.descendInput(node.value).asUnknown()}: {\n`;
const caseFrame = new Frame(false);
caseFrame.isBreakable = true;
this.descendStack(node.stack, caseFrame);
this.source += `}\n`;
break;
}
case 'control.break': {
let foundBreakable = false;
for (let i = this.frames.length - 1; i >= 0; i--) {
const frame = this.frames[i];
if (frame.isLoop || frame.isBreakable) {
foundBreakable = true;
break;
}
}
if (foundBreakable) {
this.source += `break;\n`;
}
break;
}
Step 4: Add to Block Palette (scratch-gui)
To make blocks visible in the toolbox, add them to the toolbox XML generation.
File: scratch-gui/src/lib/make-toolbox-xml.js
const control = function (isInitialSetup, isStage, targetId, colors) {
return `
<category
name="%{BKY_CATEGORY_CONTROL}"
id="control"
colour="${colors.primary}"
secondaryColour="${colors.tertiary}">
<!-- Existing blocks -->
${blockSeparator}
<block type="control_switch">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">value</field>
</shadow>
</value>
</block>
<block type="control_case">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">case</field>
</shadow>
</value>
</block>
<block type="control_default"/>
<block type="control_break"/>
</category>
`;
};
Step 5: Add Localization
Add text strings for the new blocks to support multiple languages.
Message Definitions
File: scratch-blocks/msg/messages.js
Blockly.Msg.CONTROL_SWITCH = 'switch %1';
Blockly.Msg.CONTROL_CASE = 'case %1';
Blockly.Msg.CONTROL_DEFAULT = 'default';
Blockly.Msg.CONTROL_BREAK = 'break';
English Localization
File: scratch-blocks/msg/js/en.js
Blockly.Msg["CONTROL_SWITCH"] = "switch %1";
Blockly.Msg["CONTROL_CASE"] = "case %1";
Blockly.Msg["CONTROL_DEFAULT"] = "default";
Blockly.Msg["CONTROL_BREAK"] = "break";
Block Types and Properties
Block Types
BlockType.COMMAND- Statements that execute actionsBlockType.REPORTER- Blocks that return valuesBlockType.BOOLEAN- Blocks that return true/falseBlockType.HAT- Event blocks that start scriptsBlockType.CONDITIONAL- Blocks with conditional branches
Input Types
input_value- Accepts reporter blocksinput_statement- Accepts command blocks (substack)field_dropdown- Dropdown menufield_variable- Variable picker
Extensions
colours_control- Apply control category colorsshape_statement- Standard command block shapeshape_hat- Hat block shapeoutput_string- String reporter shape
Advanced Features
Stack Frame Management
For blocks that need to maintain state across yields:
someBlock(args, util) {
// Initialize on first run
if (typeof util.stackFrame.counter === 'undefined') {
util.stackFrame.counter = 0;
}
// Use the state
util.stackFrame.counter++;
if (util.stackFrame.counter < 10) {
util.startBranch(1, true); // Loop
}
}
Reporter Blocks
For blocks that return values:
getPrimitives() {
return {
my_reporter: this.myReporter
};
}
myReporter(args, util) {
return "some value";
}
Thread Management
For blocks that control script execution:
stopScript(args, util) {
util.thread.status = Thread.STATUS_DONE;
}
Example: Switch/Case Implementation
The switch/case blocks demonstrate a complete implementation:
- Switch block - Stores the switch value in stack frame
- Case blocks - Compare their value to the switch value
- Default block - Executes when no cases match
- Break block - Exits the switch statement
This creates a familiar programming construct within Scratch's visual environment.
Testing
After implementing new blocks:
- Test in both interpreted and compiled modes
- Verify blocks appear in the correct category
- Test edge cases and error conditions
- Ensure proper cleanup of stack frames
- Test with different project types and targets
Best Practices
- Follow existing naming conventions (
category_blockname) - Use appropriate block shapes for the block's purpose
- Implement proper error handling
- Add meaningful documentation comments
- Consider performance implications
- Test with real projects, not just isolated cases