diff --git a/src/lib/monitor-adapter.js b/src/lib/monitor-adapter.js
new file mode 100644
index 0000000000000000000000000000000000000000..eccf540be8c74b52bcc8966c06ca60d765d4aad8
--- /dev/null
+++ b/src/lib/monitor-adapter.js
@@ -0,0 +1,31 @@
+/**
+ * Convert monitors from VM format to what the GUI needs to render.
+ * - Convert opcode to a label and a category
+ * - Add missing XY position data if needed
+ */
+const log = require('./log');
+
+const OpcodeLabels = require('../lib/opcode-labels.js');
+
+const PADDING = 5;
+const MONITOR_HEIGHT = 23;
+
+const isUndefined = a => typeof a === 'undefined';
+
+module.exports = function ({id, opcode, value, x, y}, monitorIndex) {
+
+    // Look up category and label from opcode label file
+    let opcodeData = OpcodeLabels[opcode];
+    if (isUndefined(opcodeData)) {
+        log.error(`No category/label found for opcode: ${opcode}`);
+        opcodeData = {category: 'data', label: opcode};
+    }
+    const {label, category} = opcodeData;
+
+    // Simple layout if x or y are undefined
+    // @todo scratch2 has a more complex layout behavior we may want to adopt
+    if (isUndefined(x)) x = PADDING;
+    if (isUndefined(y)) y = PADDING + (monitorIndex * (PADDING + MONITOR_HEIGHT));
+
+    return {id, label, category, value, x, y};
+};
diff --git a/src/lib/opcode-labels.js b/src/lib/opcode-labels.js
new file mode 100644
index 0000000000000000000000000000000000000000..402d6e56e02d1d04ae42bd56d22b41a1f6186c27
--- /dev/null
+++ b/src/lib/opcode-labels.js
@@ -0,0 +1,67 @@
+module.exports = {
+    // Motion
+    motion_direction: {
+        category: 'motion',
+        label: 'direction'
+    },
+    motion_xposition: {
+        category: 'motion',
+        label: 'x position'
+    },
+    motion_yposition: {
+        category: 'motion',
+        label: 'y position'
+    },
+
+    // Looks
+    looks_size: {
+        category: 'looks',
+        label: 'size'
+    },
+    looks_costumeorder: {
+        category: 'looks',
+        label: 'costume #'
+    },
+    looks_backdroporder: {
+        category: 'looks',
+        label: 'backdrop #'
+    },
+    looks_backdropname: {
+        category: 'looks',
+        label: 'backdrop name'
+    },
+
+    // Data
+    data_variable: {
+        category: 'data',
+        label: 'Variable _' // @todo placeholder for params
+    },
+
+    // Sound
+    sound_volume: {
+        category: 'sound',
+        label: 'volume'
+    },
+    sound_tempo: {
+        category: 'sound',
+        label: 'tempo'
+    },
+
+    // Sensing
+    sensing_loudness: {
+        category: 'sensing',
+        label: 'loundness'
+    },
+    sensing_of: {
+        category: 'sensing',
+        label: '_ of _' // @todo placeholder for params
+    },
+    sensing_current: {
+        category: 'sensing',
+        label: 'current _' // @todo placeholder for param
+    },
+    sensing_timer: {
+        category: 'timer',
+        label: 'timer'
+    }
+};
diff --git a/src/reducers/monitors.js b/src/reducers/monitors.js
index e8b4fa947eee4862f24be5ea8727caef6793fe5c..18e9529f4ad417645101a5cc85ed60b6b8adecf0 100644
--- a/src/reducers/monitors.js
+++ b/src/reducers/monitors.js
@@ -1,3 +1,5 @@
+const monitorAdapter = require('../lib/monitor-adapter.js');
+
 const UPDATE_MONITORS = 'scratch-gui/monitors/UPDATE_MONITORS';
 
 const initialState = [];
@@ -6,7 +8,7 @@ const reducer = function (state, action) {
     if (typeof state === 'undefined') state = initialState;
     switch (action.type) {
     case UPDATE_MONITORS:
-        return [...action.monitors];
+        return action.monitors.map(monitorAdapter);
     default:
         return state;
     }