diff --git a/package.json b/package.json
index 3ae8de45afa2d36bbc84c21792c4c0d1662634bf..d293d00de07edb67468d04e1030c09b769c83455 100644
--- a/package.json
+++ b/package.json
@@ -24,14 +24,14 @@
     "url": "git+ssh://git@github.com/LLK/scratch-gui.git"
   },
   "peerDependencies": {
-    "react": "^15.6.0",
-    "react-dom": "^15.6.0"
+    "react": "^15.6.1",
+    "react-dom": "^15.6.1"
   },
   "devDependencies": {
     "autoprefixer": "7.1.2",
-    "babel-cli": "6.24.1",
+    "babel-cli": "6.26.0",
     "babel-core": "^6.23.1",
-    "babel-eslint": "^7.1.1",
+    "babel-eslint": "^7.2.3",
     "babel-loader": "^7.1.0",
     "babel-plugin-react-intl": "2.3.1",
     "babel-plugin-syntax-dynamic-import": "6.18.0",
@@ -42,13 +42,12 @@
     "chromedriver": "^2.31.0",
     "classnames": "2.2.5",
     "copy-webpack-plugin": "4.0.1",
-    "css-loader": "0.28.3",
+    "css-loader": "0.28.5",
     "enzyme": "^2.8.2",
-    "eslint": "^3.16.1",
-    "eslint-config-import": "^0.13.0",
-    "eslint-config-scratch": "^3.0.0",
+    "eslint": "^4.4.1",
+    "eslint-config-scratch": "^4.0.0",
     "eslint-plugin-import": "^2.7.0",
-    "eslint-plugin-react": "^7.0.1",
+    "eslint-plugin-react": "^7.2.1",
     "get-float-time-domain-data": "0.1.0",
     "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
     "html-webpack-plugin": "2.30.0",
@@ -66,9 +65,9 @@
     "postcss-loader": "^2.0.5",
     "postcss-simple-vars": "^4.0.0",
     "prop-types": "^15.5.10",
-    "react": "15.5.4",
+    "react": "15.6.1",
     "react-contextmenu": "2.6.5",
-    "react-dom": "15.5.4",
+    "react-dom": "15.6.1",
     "react-draggable": "2.2.6",
     "react-intl": "2.3.0",
     "react-intl-redux": "0.6.0",
diff --git a/src/.eslintrc.js b/src/.eslintrc.js
index e558dde263eb1f6970b0abcbf62e9f28ab655580..ff88f0f27fa8ea14df758aed45b27ba9460806d5 100644
--- a/src/.eslintrc.js
+++ b/src/.eslintrc.js
@@ -1,6 +1,6 @@
 module.exports = {
     root: true,
-    extends: ['scratch', 'scratch/es6', 'scratch/react', 'import'],
+    extends: ['scratch', 'scratch/es6', 'scratch/react', 'plugin:import/errors'],
     env: {
         browser: true
     },
diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css
index 454887ab3da3d20c6d125c6d89cc8815a7cf094d..fe276c3819eed948b33d34cde90da60f19bca77f 100644
--- a/src/components/audio-trimmer/audio-trimmer.css
+++ b/src/components/audio-trimmer/audio-trimmer.css
@@ -16,6 +16,7 @@ $hover-scale: 2;
 
 .trim-background {
     cursor: pointer;
+    touch-action: none;
 }
 
 .trim-background-mask {
diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx
index b7bbe380fe26b0371edd7a03eac205a0aa71b33d..d1b21c92664cebc586212c9833629ba90da8f701 100644
--- a/src/components/audio-trimmer/audio-trimmer.jsx
+++ b/src/components/audio-trimmer/audio-trimmer.jsx
@@ -17,6 +17,7 @@ const AudioTrimmer = props => (
                     width: `${100 * props.trimStart}%`
                 }}
                 onMouseDown={props.onTrimStartMouseDown}
+                onTouchStart={props.onTrimStartMouseDown}
             >
                 <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
                 <Box className={classNames(styles.trimLine, styles.startTrimLine)}>
@@ -47,6 +48,7 @@ const AudioTrimmer = props => (
                     width: `${100 - (100 * props.trimEnd)}%`
                 }}
                 onMouseDown={props.onTrimEndMouseDown}
+                onTouchStart={props.onTrimEndMouseDown}
             >
                 <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
                 <Box className={classNames(styles.trimLine, styles.endTrimLine)}>
diff --git a/src/components/close-button/close-button.css b/src/components/close-button/close-button.css
index b679f8fb27a1bfddb9a7b6895efa5b6315855951..1864e7b2aaede98cf6f4b827db6ba63586275f47 100644
--- a/src/components/close-button/close-button.css
+++ b/src/components/close-button/close-button.css
@@ -9,7 +9,6 @@
     overflow: hidden;  /* Mask the icon animation */
     background-color: rgba(0, 0, 0, 0.1);
     border-radius: 50%;
-    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     user-select: none;
     cursor: pointer;
@@ -22,8 +21,8 @@
 }
 
 .small {
-    width: 1rem;
-    height: 1rem;
+    width: 0.825rem;
+    height: 0.825rem;
     color: #FFF;
     background-color: $motion-primary;
 }
@@ -31,6 +30,7 @@
 .large {
     width: 1.75rem;
     height: 1.75rem;
+    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
 }
 
 .close-icon {
@@ -42,7 +42,7 @@
 }
 
 .small .close-icon {
-    width: 40%;
+    width: 50%;
 }
 
 .large .close-icon {
diff --git a/src/components/controls/controls.css b/src/components/controls/controls.css
new file mode 100644
index 0000000000000000000000000000000000000000..7e60ae33419c73f93a82da5e6d1c21e7c87d6a6a
--- /dev/null
+++ b/src/components/controls/controls.css
@@ -0,0 +1,3 @@
+.controls-container {
+    display: flex;
+}
diff --git a/src/components/controls/controls.jsx b/src/components/controls/controls.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8fec705b08ac9ad19a3d5a1669fded868b4b8a3a
--- /dev/null
+++ b/src/components/controls/controls.jsx
@@ -0,0 +1,53 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import GreenFlag from '../green-flag/green-flag.jsx';
+import StopAll from '../stop-all/stop-all.jsx';
+import TurboMode from '../turbo-mode/turbo-mode.jsx';
+
+import styles from './controls.css';
+
+const Controls = function (props) {
+    const {
+        active,
+        className,
+        onGreenFlagClick,
+        onStopAllClick,
+        turbo,
+        ...componentProps
+    } = props;
+    return (
+        <div
+            className={classNames(styles.controlsContainer, className)}
+            {...componentProps}
+        >
+            <GreenFlag
+                active={active}
+                onClick={onGreenFlagClick}
+            />
+            <StopAll
+                active={active}
+                onClick={onStopAllClick}
+            />
+            {turbo ? (
+                <TurboMode />
+            ) : null}
+        </div>
+    );
+};
+
+Controls.propTypes = {
+    active: PropTypes.bool,
+    className: PropTypes.string,
+    onGreenFlagClick: PropTypes.func.isRequired,
+    onStopAllClick: PropTypes.func.isRequired,
+    turbo: PropTypes.bool
+};
+
+Controls.defaultProps = {
+    active: false,
+    turbo: false
+};
+
+export default Controls;
diff --git a/src/components/costume-canvas/costume-canvas.jsx b/src/components/costume-canvas/costume-canvas.jsx
index eafe92930f7eaf5f4dbfcd246b93ef7d6796bb25..a142e8d27e642fbd4225d2e8f7a87ab6cf1ab34f 100644
--- a/src/components/costume-canvas/costume-canvas.jsx
+++ b/src/components/costume-canvas/costume-canvas.jsx
@@ -21,7 +21,7 @@ class CostumeCanvas extends React.Component {
             prevProps.width !== this.props.width ||
             prevProps.height !== this.props.height ||
             prevProps.direction !== this.props.direction
-          ) {
+        ) {
             this.draw();
         }
     }
diff --git a/src/components/forms/input.css b/src/components/forms/input.css
index 91b42d4cc59e69dc505975a90d22db22891b5253..6d5eefa52d5b2f4d5c0c558fb37aa1f05ad5e7c0 100644
--- a/src/components/forms/input.css
+++ b/src/components/forms/input.css
@@ -2,11 +2,13 @@
 @import "../../css/colors.css";
 
 .input-form {
-    padding: $space 0.75rem;
+    height: 2rem;
+    padding: 0 0.75rem;
 
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     font-size: 0.625rem;
     font-weight: bold;
+    color: $text-primary;
 
     border-width: 1px;
     border-style: solid;
diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css
index 07e57621bbbe2b115812827f22497a6398fb0e49..2c145283869ac0788d3e5be4f96e8b6afd04594f 100644
--- a/src/components/green-flag/green-flag.css
+++ b/src/components/green-flag/green-flag.css
@@ -1,14 +1,21 @@
 .green-flag {
-    width: 1.1rem;
-    height: 1.1rem;
-    opacity: 0.5;
-    margin: 0.25rem 0.6rem;
+    box-sizing: content-box;
+    width: 1.25rem;
+    height: 1.25rem;
+    padding: 0.375rem;
+    border-radius: 0.25rem;
     user-select: none;
     cursor: pointer;
-    transition: opacity 0.2s ease-out; /* @todo: standardize with var */ 
+    transition: 0.2s ease-out;
 }
 
-.green-flag.is-active,
 .green-flag:hover {
-    opacity: 1;
+    /* Scale flag image by 1.2, but keep background static */
+    width: 1.5rem;
+    height: 1.5rem;
+    padding: 0.25rem;
+}
+
+.green-flag.is-active {
+    background-color: rgba(0, 0, 0, 0.1);
 }
diff --git a/src/components/green-flag/green-flag.jsx b/src/components/green-flag/green-flag.jsx
index 692a347cd1eac1b8c33a56973c50bf0ca256d7c6..6b8733c1db116fd53c4a91970206e8596244cd6c 100644
--- a/src/components/green-flag/green-flag.jsx
+++ b/src/components/green-flag/green-flag.jsx
@@ -8,16 +8,20 @@ import styles from './green-flag.css';
 const GreenFlagComponent = function (props) {
     const {
         active,
+        className,
         onClick,
         title,
         ...componentProps
     } = props;
     return (
         <img
-            className={classNames({
-                [styles.greenFlag]: true,
-                [styles.isActive]: active
-            })}
+            className={classNames(
+                className,
+                styles.greenFlag,
+                {
+                    [styles.isActive]: active
+                }
+            )}
             src={greenFlagIcon}
             title={title}
             onClick={onClick}
@@ -27,6 +31,7 @@ const GreenFlagComponent = function (props) {
 };
 GreenFlagComponent.propTypes = {
     active: PropTypes.bool,
+    className: PropTypes.string,
     onClick: PropTypes.func.isRequired,
     title: PropTypes.string
 };
diff --git a/src/components/green-flag/icon-green-flag.svg b/src/components/green-flag/icon-green-flag.svg
deleted file mode 100644
index 4bd2528f9be697e4b8a35158c217fffe81c8309b..0000000000000000000000000000000000000000
Binary files a/src/components/green-flag/icon-green-flag.svg and /dev/null differ
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index 5146c84f16e5340ab0be74a4340f40c3533ebbd6..c8a9285a2a204bee92158053e3c268c4bc4967cd 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -123,10 +123,7 @@
 }
 
 .target-wrapper {
-    /*  Take all the available vertical space available.
-        Works in tandem with height: 100%; which is set on the child: .targetPane
-        @todo: Not working in Safari, not great in FFx
-    */
+    display: flex;
     flex-grow: 1;
     flex-basis: 0;
 
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 1e7fe027e54d69c1caead489e3bb5f9f6dde6fdf..1eb0e65054565751b964170a6cc989b46869364a 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -8,11 +8,10 @@ import VM from 'scratch-vm';
 
 import Blocks from '../../containers/blocks.jsx';
 import CostumeTab from '../../containers/costume-tab.jsx';
-import GreenFlag from '../../containers/green-flag.jsx';
+import Controls from '../../containers/controls.jsx';
 import TargetPane from '../../containers/target-pane.jsx';
 import SoundTab from '../../containers/sound-tab.jsx';
 import Stage from '../../containers/stage.jsx';
-import StopAll from '../../containers/stop-all.jsx';
 
 import Box from '../box/box.jsx';
 import MenuBar from '../menu-bar/menu-bar.jsx';
@@ -90,8 +89,7 @@ const GUIComponent = props => {
 
                     <Box className={styles.stageAndTargetWrapper}>
                         <Box className={styles.stageMenuWrapper}>
-                            <GreenFlag vm={vm} />
-                            <StopAll vm={vm} />
+                            <Controls vm={vm} />
                         </Box>
                         <Box className={styles.stageWrapper}>
                             <MediaQuery minWidth={layout.fullSizeMinWidth}>{isFullSize => (
diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css
index 12253a55202602010b71b2a98014d1ecbbe787df..067b2c1af3328a80dd5e228d810d6129f1788617 100644
--- a/src/components/library-item/library-item.css
+++ b/src/components/library-item/library-item.css
@@ -1,3 +1,4 @@
+@import "../../css/colors.css";
 @import "../../css/units.css";
 
 .library-item {
@@ -11,7 +12,7 @@
     margin: $space;
     padding: 1rem 1rem 0 1rem;
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-    color: #575e75;
+    color: $text-primary;
     background: white;
     border-width: 2px;
     border-style: solid;
diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css
index 88fb5773f14dc0589be7a2796f5a3d9b6a900fe5..257c59d655357a096638cc18c6ac6b4dbde5a3b6 100644
--- a/src/components/sound-editor/sound-editor.css
+++ b/src/components/sound-editor/sound-editor.css
@@ -49,7 +49,7 @@ $border-radius: 0.25rem;
     transition: 0.2s;
 }
 
-.button img {
+.button > img {
     flex-grow: 1;
     max-width: 100%;
     max-height: 100%;
@@ -62,6 +62,10 @@ $border-radius: 0.25rem;
     padding-right: 10px; /* To equalize with empty whitespace from image on left */
 }
 
+.trim-button > img {
+    width: 1.5rem;
+}
+
 .trim-button-active {
     filter: hue-rotate(155deg); /* @todo replace blue -> red with real submit icon */
 }
@@ -74,7 +78,7 @@ $border-radius: 0.25rem;
 
 .effect-button {
     flex-basis: 150px;
-    color: #575e75; /* @todo discuss the multiple font colors with Carl, move to variable */
+    color: $text-primary;
 }
 
 .effect-button + .effect-button {
diff --git a/src/components/sprite-info/icon--hide.svg b/src/components/sprite-info/icon--hide.svg
index 9463538707630dcc89773e8e0f9c5800a22a2a64..2fd66b8300966bd93aae01b51358b760d5815ec4 100644
Binary files a/src/components/sprite-info/icon--hide.svg and b/src/components/sprite-info/icon--hide.svg differ
diff --git a/src/components/sprite-info/icon--show.svg b/src/components/sprite-info/icon--show.svg
index db06b704809d81c056f3873dcb1583ffc82afb9a..fe54ed568913266746b3086cc649164218523a3e 100644
Binary files a/src/components/sprite-info/icon--show.svg and b/src/components/sprite-info/icon--show.svg differ
diff --git a/src/components/sprite-info/icon--x.svg b/src/components/sprite-info/icon--x.svg
index 4d3cbc3a1cd7c5ddd964416e8d4fe750d1b924f2..57b1852f587167e33499b395c083939c4ae21059 100644
Binary files a/src/components/sprite-info/icon--x.svg and b/src/components/sprite-info/icon--x.svg differ
diff --git a/src/components/sprite-info/icon--y.svg b/src/components/sprite-info/icon--y.svg
index 5fe7df308aa6b56b20450b97093ca767bf5c08e2..f8eab05ef0a6b673869d6db5ffaa321029f98d9d 100644
Binary files a/src/components/sprite-info/icon--y.svg and b/src/components/sprite-info/icon--y.svg differ
diff --git a/src/components/sprite-info/sprite-info.css b/src/components/sprite-info/sprite-info.css
index e6a758d1c44936e7f3a35a9faa7a2cda6ee6a2a8..200aedaf79f13627abf74a4c9378ccb0f60eb300 100644
--- a/src/components/sprite-info/sprite-info.css
+++ b/src/components/sprite-info/sprite-info.css
@@ -3,10 +3,10 @@
 
 .sprite-info {
     height: $sprite-info-height;
-    padding: $space;
+    padding: 0.75rem;
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     background-color: white;
-    color: #575e75;
+    color: $text-primary;
     border-top-left-radius: $space;
     border-top-right-radius: $space;
     border-bottom: 1px solid #eaeaea;
@@ -17,7 +17,9 @@
     justify-content: space-between;
 }
 
-.row-primary { margin-bottom: $space; }
+.row-primary {
+    margin-bottom: 0.5rem;
+}
 
 .label { opacity: 0.8; }
 
@@ -29,15 +31,10 @@
 
 .icon-wrapper {
     display: inline-block;
-
-    /*
-        content-box is normally the browser's default.
-        We're overriding the global, which we set to border-box
-    */
     box-sizing: content-box;
-    width: 1.25rem;
-    height: 1.25rem;
-    padding: calc($space / 2);
+    width: 1rem;
+    height: 1rem;
+    padding: 0.5rem;
     outline: none;
     user-select: none;
 }
@@ -48,9 +45,16 @@
 }
 
 /* @todo: refactor radio divs to input */
-.radio { opacity: 0.4; cursor: pointer; }
-.radio.is-active   { opacity: 1; }
-.radio.is-disabled { cursor: default; }
+.radio {
+    filter: saturate(0);
+    cursor: pointer;
+}
+.radio.is-active {
+    filter: none;
+}
+.radio.is-disabled {
+    cursor: default;
+}
 
 .radio-left {
     border: 1px solid $form-border;
diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx
index 8b3f212d99dfa870db88c6338f4efd09cfe0a643..f9bbb64e32748af1f955db724d8dc113caea8648 100644
--- a/src/components/sprite-info/sprite-info.jsx
+++ b/src/components/sprite-info/sprite-info.jsx
@@ -97,49 +97,50 @@ class SpriteInfo extends React.Component {
 
                 <div className={classNames(styles.row, styles.rowSecondary)}>
                     <div className={styles.group}>
-                        <Label
-                            secondary
-                            text="Show"
-                        >
-                            <div>
-                                <div
-                                    className={classNames(
-                                        styles.radio,
-                                        styles.radioLeft,
-                                        styles.iconWrapper,
-                                        {
-                                            [styles.isActive]: this.props.visible && !this.props.disabled,
-                                            [styles.isDisabled]: this.props.disabled
-                                        }
-                                    )}
-                                    tabIndex="4"
-                                    onClick={this.props.onClickVisible}
-                                >
-                                    <img
-                                        className={styles.icon}
-                                        src={showIcon}
-                                    />
-                                </div>
-                                <div
-                                    className={classNames(
-                                        styles.radio,
-                                        styles.radioRight,
-                                        styles.iconWrapper,
-                                        {
-                                            [styles.isActive]: !this.props.visible && !this.props.disabled,
-                                            [styles.isDisabled]: this.props.disabled
-                                        }
-                                    )}
-                                    tabIndex="4"
-                                    onClick={this.props.onClickNotVisible}
-                                >
-                                    <img
-                                        className={styles.icon}
-                                        src={hideIcon}
-                                    />
-                                </div>
+                        <MediaQuery minWidth={layout.fullSizeMinWidth}>
+                            <Label
+                                secondary
+                                text="Show"
+                            />
+                        </MediaQuery>
+                        <div>
+                            <div
+                                className={classNames(
+                                    styles.radio,
+                                    styles.radioLeft,
+                                    styles.iconWrapper,
+                                    {
+                                        [styles.isActive]: this.props.visible && !this.props.disabled,
+                                        [styles.isDisabled]: this.props.disabled
+                                    }
+                                )}
+                                tabIndex="4"
+                                onClick={this.props.onClickVisible}
+                            >
+                                <img
+                                    className={styles.icon}
+                                    src={showIcon}
+                                />
                             </div>
-                        </Label>
+                            <div
+                                className={classNames(
+                                    styles.radio,
+                                    styles.radioRight,
+                                    styles.iconWrapper,
+                                    {
+                                        [styles.isActive]: !this.props.visible && !this.props.disabled,
+                                        [styles.isDisabled]: this.props.disabled
+                                    }
+                                )}
+                                tabIndex="4"
+                                onClick={this.props.onClickNotVisible}
+                            >
+                                <img
+                                    className={styles.icon}
+                                    src={hideIcon}
+                                />
+                            </div>
+                        </div>
                     </div>
                     <div className={styles.group}>
                         <Label
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
index faf7c36125223cbd400d2ae1c64fb40d8a58d51b..60f8393565b8d2e4068a005dd5bc65f4e00dbef4 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.css
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -1,4 +1,5 @@
 @import "../../css/units.css";
+@import "../../css/colors.css";
 
 /* @todo: refactor this class name, and component: `sprite-selector` to `sprite` */
 .sprite-selector-item {
@@ -17,22 +18,16 @@
     border-radius: $space;
     text-align: center;
     cursor: pointer;
-}
-
-.sprite-selector-item:hover {
-    border-width: 2px;
-    border-color: #1dacf4;
     transition: 0.25s ease-out;
 }
 
-/* @todo: refactor out descendent selectors into regular classes */
 .sprite-selector-item.is-selected {
-    border-width: 2px;
-    border-color: #1dacf4;
-    transition: 0.25s ease-out;
+    border: 2px solid $motion-primary;
+    box-shadow: 0px 0px 0px 3px $motion-transparent;
 }
-.sprite-selector-item.is-selected .info-button {
-    display: block;
+
+.sprite-selector-item:hover {
+    border: 2px solid $motion-primary;
 }
 
 .sprite-image {
@@ -44,7 +39,7 @@
     font-size: 0.625rem;
     margin: 0.15rem;
     user-select: none;
-    
+
     /*
         For truncating overflowing text gracefully
         Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
index 37e80217126cd251f3e1b09ca6987659f9cdae71..2db6a7feacdb0b0b04babe8e07aa7bea9ecb2990 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.jsx
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -28,7 +28,7 @@ const SpriteSelectorItem = props => (
                 size={CloseButton.SIZE_SMALL}
                 onClick={props.onDeleteButtonClick}
             />
-            ) : null }
+        ) : null }
         {props.costumeURL ? (
             <CostumeCanvas
                 className={styles.spriteImage}
@@ -36,7 +36,7 @@ const SpriteSelectorItem = props => (
                 url={props.costumeURL}
                 width={32}
             />
-            ) : null}
+        ) : null}
         <div className={styles.spriteName}>{props.name}</div>
         <ContextMenu id={`${props.name}-${contextMenuId++}`}>
             <MenuItem onClick={props.onDeleteButtonClick}>
diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css
index 496d19588341fa4b7ae87bef8389b171d1efb9fb..8c1e2315a4dd79294d91585154301fbce4ffe820 100644
--- a/src/components/sprite-selector/sprite-selector.css
+++ b/src/components/sprite-selector/sprite-selector.css
@@ -30,7 +30,7 @@
     */
     box-sizing: border-box;
     width: calc((100% / $sprites-per-row ) - $space);
-
+    min-width: 4rem;
     min-height: 4rem; /* @todo: calc height same as width */
     margin: calc($space / 2); 
 }
@@ -64,6 +64,6 @@
     font-size: 0.55rem;
     font-weight: bold;
     position: absolute;
-    bottom: 0.5rem;
-    right: 0.5rem;
+    bottom: 0.75rem;
+    right: 1rem;
 }
diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css
index f3f01a5eb6e86a2959673ebb32653c1792a851e9..73df5305ff6823fbfbd70e665bfb1e183f98af1a 100644
--- a/src/components/stage-selector/stage-selector.css
+++ b/src/components/stage-selector/stage-selector.css
@@ -1,19 +1,34 @@
 @import "../../css/units.css";
+@import "../../css/colors.css";
 
-$header-height: 2.5rem; /* @todo: half the SpriteInfo area header? */
+$header-height: calc($stage-menu-height - 2px);
 
 .stage-selector {
-    position: relative; /* for the child element border */
-    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    position: relative; /* For the add backdrop button */
+    flex-grow: 1;
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-    background-color: white;
-    color: #575e75;
+    background-color: #fff;
+    color: $text-primary;
     border-top-left-radius: $space;
     border-top-right-radius: $space;
     border-color: #dbdbdb;
     border-width: 1px;
     border-style: solid;
     border-bottom: 0;
+    cursor: pointer;
+    transition: border-color 0.25s ease-out, box-shadow 0.25s ease-out;
+}
+
+.stage-selector.is-selected {
+    border-color: $motion-primary;
+    box-shadow: 0px 0px 0px 3px $motion-transparent;
+}
+
+.stage-selector:hover {
+    border-color: $motion-primary;
 }
 
 .header {
@@ -22,82 +37,45 @@ $header-height: 2.5rem; /* @todo: half the SpriteInfo area header? */
     align-items: center;
     justify-content: center;
     height: $header-height;
-    padding: 0.5rem 0.5rem 0.5rem 0.25rem;
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     background-color: white;
-    color: #575e75;
+    color: $text-primary;
     border-top-left-radius: $space;
     border-top-right-radius: $space;
     border-bottom: 1px solid #eaeaea;
+    width: 100%;
 }
 
 .header-title {
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     font-size: 0.625rem;
     font-weight: bold;
-    color: #575e75;
+    color: $text-primary;
 
     /* @todo: make this a mixin for all UI text labels */
     user-select: none;
-    cursor: default;
-}
-
-.body {
-    justify-content: space-around;
-    padding: $space;
-    height: calc(100% - $header-height);
-    background-color: #f9f9f9;
 }
 
 .count {
-    margin: 0 0 0.3rem 0;
-    padding: 0.15rem 0.5rem;
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    padding: 0.3rem 0.75rem;
     font-size: 0.625rem;
-    font-weight: bold;
-    color: #575e75;
-    background: #ededed;
+    color: $text-primary;
+    background: white;
+    border: 1px solid #eaeaea;
     border-radius: 0.25rem;
     user-select: none;
 }
 
 .label {
-    margin: 0.4rem 0 0.25rem;
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-size: 0.5rem;
-    font-weight: bold;
-    color: #575e75;
+    margin: 0.75rem 0 0.25rem;
+    font-size: 0.6rem;
+    color: $text-primary;
     user-select: none;
 }
 
-$border-width: 2px;
-
-.flex-wrapper {
-    display: flex;
-    flex-direction: column; /* makes rows */
-    align-items: center;
-    overflow: hidden;
-    background-color: white;
-    border-radius: calc($space / 2);
-    border-width: $border-width;
-    border-style: solid;
-    border-color: #e9eef2;
-    cursor: pointer;
-    transition: border-color 0.1s ease-out;
-}
-
-.flex-wrapper:hover {
-    border-color: #1dacf4;
-}
-
-.flex-wrapper.is-selected {
-    border-color: #1dacf4;
-}
-
 .costume-canvas {
     display: block;
     width: 100%;
     user-select: none;
+    border-bottom: 1px solid #eaeaea;
 }
 
 .add-button {
@@ -105,6 +83,6 @@ $border-width: 2px;
     font-weight: bold;
     text-align: center;
     position: absolute;
-    bottom: 0.5rem;
+    bottom: 0.75rem;
     left: 0.25rem;
 }
diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx
index ec5fbb36d2d6d387c0477a1aa55d8a0ccc0a1459..93002f9efb461e2b141104024711df8f5bc0b625 100644
--- a/src/components/stage-selector/stage-selector.jsx
+++ b/src/components/stage-selector/stage-selector.jsx
@@ -28,38 +28,37 @@ const StageSelector = props => {
     } = props;
     return (
         <Box
-            className={styles.stageSelector}
+            className={classNames(styles.stageSelector, {
+                [styles.isSelected]: selected
+            })}
             onClick={onClick}
             {...componentProps}
         >
             <div className={styles.header}>
                 <div className={styles.headerTitle}>Stage</div>
             </div>
-            <div className={styles.body}>
-                <div
-                    className={classNames({
-                        [styles.flexWrapper]: true,
-                        [styles.isSelected]: selected
-                    })}
-                >
-                    {url ? (
-                        <CostumeCanvas
-                            className={styles.costumeCanvas}
-                            height={42}
-                            url={url}
-                            width={56}
-                        />
-                    ) : null}
-                    <div className={styles.label}>Backdrops</div>
-                    <div className={styles.count}>{backdropCount}</div>
-                </div>
-                <IconButton
-                    className={styles.addButton}
-                    img={backdropIcon}
-                    title={addBackdropMessage}
-                    onClick={onNewBackdropClick}
+            {url ? (
+                <CostumeCanvas
+                    className={styles.costumeCanvas}
+                    height={42}
+                    url={url}
+                    width={56}
+                />
+            ) : null}
+            <div className={styles.label}>
+                <FormattedMessage
+                    defaultMessage="Backdrops"
+                    description="Label for the backdrops in the stage selector"
+                    id="stageSelector.backdrops"
                 />
             </div>
+            <div className={styles.count}>{backdropCount}</div>
+            <IconButton
+                className={styles.addButton}
+                img={backdropIcon}
+                title={addBackdropMessage}
+                onClick={onNewBackdropClick}
+            />
         </Box>
     );
 };
diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css
index 95a63377110218a2977663572866915e599a537a..99ebee89284ebd481597ef86874083352b2dc352 100644
--- a/src/components/stop-all/stop-all.css
+++ b/src/components/stop-all/stop-all.css
@@ -1,13 +1,22 @@
 .stop-all {
-    width: 1.1rem;
-    height: 1.1rem;
-    opacity: 0.5;
+    box-sizing: content-box;
+    width: 1.25rem;
+    height: 1.25rem;
+    padding: 0.375rem;
+    border-radius: 0.25rem;
     user-select: none;
     cursor: pointer;
-    transition: opacity 0.2s ease-out; /* @todo: standardize with var */ 
+    transition: 0.2s ease-out;
+}
+
+.stop-all {
+    opacity: 0.5;
 }
 
-.stop-all.is-active,
 .stop-all:hover {
+    transform: scale(1.2);
+}
+
+.stop-all.is-active {
     opacity: 1;
 }
diff --git a/src/components/stop-all/stop-all.jsx b/src/components/stop-all/stop-all.jsx
index aba3ed42415c51456610ea5a892ddaccd0927921..f2916f605010441b3f7c5985ff5045dae1d5861f 100644
--- a/src/components/stop-all/stop-all.jsx
+++ b/src/components/stop-all/stop-all.jsx
@@ -8,16 +8,20 @@ import styles from './stop-all.css';
 const StopAllComponent = function (props) {
     const {
         active,
+        className,
         onClick,
         title,
         ...componentProps
     } = props;
     return (
         <img
-            className={classNames({
-                [styles.stopAll]: true,
-                [styles.isActive]: active
-            })}
+            className={classNames(
+                className,
+                styles.stopAll,
+                {
+                    [styles.isActive]: active
+                }
+            )}
             src={stopAllIcon}
             title={title}
             onClick={onClick}
@@ -28,6 +32,7 @@ const StopAllComponent = function (props) {
 
 StopAllComponent.propTypes = {
     active: PropTypes.bool,
+    className: PropTypes.string,
     onClick: PropTypes.func.isRequired,
     title: PropTypes.string
 };
diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css
index 380200ff52503aef919dad684d0d75e1989e06f4..404283d19fd8c77bafc1e9f0b307628168d1cfc4 100644
--- a/src/components/target-pane/target-pane.css
+++ b/src/components/target-pane/target-pane.css
@@ -4,22 +4,12 @@
     /* Makes columns for the sprite library selector + and the stage selector */
     display: flex;
     flex-direction: row;
-
-    height: 100%;
+    flex-grow: 1;
 }
 
 .stage-selector-wrapper {
+    display: flex;
     flex-basis: 72px;
     flex-shrink: 0;
     margin-left: calc($space / 2);
 }
-
-.add-button-wrapper {
-    position: absolute;
-    z-index: 1;
-    bottom: 0.5rem;
-    border: 0;
-    transition: all 0.15s ease-out; /* @todo: standardize with var */
-    cursor: pointer;
-    user-select: none;
-}
diff --git a/src/components/turbo-mode/icon--turbo.svg b/src/components/turbo-mode/icon--turbo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..64fd1665fc03cb694262fc1a67b1adc958e29f4b
Binary files /dev/null and b/src/components/turbo-mode/icon--turbo.svg differ
diff --git a/src/components/turbo-mode/turbo-mode.css b/src/components/turbo-mode/turbo-mode.css
new file mode 100644
index 0000000000000000000000000000000000000000..d12699cee0764a66a5e60e86c7ec259239704571
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.css
@@ -0,0 +1,20 @@
+@import "../../css/colors.css";
+
+.turbo-container {
+    display: flex;
+    align-items: center;
+    padding: 0.25rem;
+    user-select: none;
+}
+
+.turbo-icon {
+    margin: 0.25rem;
+}
+
+.turbo-label {
+    font-size: 0.625rem;
+    font-weight: bold;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    color: $control-primary;
+    white-space: nowrap;
+}
diff --git a/src/components/turbo-mode/turbo-mode.jsx b/src/components/turbo-mode/turbo-mode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..724af24d1e1527c2acb79de0df0083d3ef1de4e7
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import turboIcon from './icon--turbo.svg';
+
+import styles from './turbo-mode.css';
+
+const TurboMode = () => (
+    <div className={styles.turboContainer}>
+        <img
+            className={styles.turboIcon}
+            src={turboIcon}
+        />
+        <div className={styles.turboLabel}>
+            <FormattedMessage
+                defaultMessage="Turbo Mode"
+                description="Label indicating turbo mode is active"
+                id="controls.turboMode"
+            />
+        </div>
+    </div>
+);
+
+export default TurboMode;
diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx
index 7d7c1e9f41ffec43082fbea67ca6174e8f6b4f0f..f4ba1be3cfb8376ee45c28ec1026b17fee56c7bf 100644
--- a/src/containers/audio-trimmer.jsx
+++ b/src/containers/audio-trimmer.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import bindAll from 'lodash.bindall';
 import AudioTrimmerComponent from '../components/audio-trimmer/audio-trimmer.jsx';
+import {getEventXY} from '../lib/touch-utils';
 
 class AudioTrimmer extends React.Component {
     constructor (props) {
@@ -18,14 +19,14 @@ class AudioTrimmer extends React.Component {
     }
     handleTrimStartMouseMove (e) {
         const containerSize = this.containerElement.getBoundingClientRect().width;
-        const dx = (e.clientX - this.initialX) / containerSize;
+        const dx = (getEventXY(e).x - this.initialX) / containerSize;
         const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx));
         this.props.onSetTrimStart(newTrim);
         e.preventDefault();
     }
     handleTrimEndMouseMove (e) {
         const containerSize = this.containerElement.getBoundingClientRect().width;
-        const dx = (e.clientX - this.initialX) / containerSize;
+        const dx = (getEventXY(e).x - this.initialX) / containerSize;
         const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx));
         this.props.onSetTrimEnd(newTrim);
         e.preventDefault();
@@ -33,22 +34,30 @@ class AudioTrimmer extends React.Component {
     handleTrimStartMouseUp () {
         window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
         window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
+        window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
+        window.removeEventListener('touchend', this.handleTrimStartMouseUp);
     }
     handleTrimEndMouseUp () {
         window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
         window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
+        window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
+        window.removeEventListener('touchend', this.handleTrimEndMouseUp);
     }
     handleTrimStartMouseDown (e) {
-        this.initialX = e.clientX;
+        this.initialX = getEventXY(e).x;
         this.initialTrim = this.props.trimStart;
         window.addEventListener('mousemove', this.handleTrimStartMouseMove);
         window.addEventListener('mouseup', this.handleTrimStartMouseUp);
+        window.addEventListener('touchmove', this.handleTrimStartMouseMove);
+        window.addEventListener('touchend', this.handleTrimStartMouseUp);
     }
     handleTrimEndMouseDown (e) {
-        this.initialX = e.clientX;
+        this.initialX = getEventXY(e).x;
         this.initialTrim = this.props.trimEnd;
         window.addEventListener('mousemove', this.handleTrimEndMouseMove);
         window.addEventListener('mouseup', this.handleTrimEndMouseUp);
+        window.addEventListener('touchmove', this.handleTrimEndMouseMove);
+        window.addEventListener('touchend', this.handleTrimEndMouseUp);
     }
     storeRef (el) {
         this.containerElement = el;
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 86d9108973e67e3f294166e0c86a4b39badae8a1..63a76276585a197941462999f26c911e31526ebe 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -208,7 +208,7 @@ class Blocks extends React.Component {
 }
 
 Blocks.propTypes = {
-    isVisible: PropTypes.bool.isRequired,
+    isVisible: PropTypes.bool,
     options: PropTypes.shape({
         media: PropTypes.string,
         zoom: PropTypes.shape({
@@ -260,6 +260,7 @@ Blocks.defaultOptions = {
 };
 
 Blocks.defaultProps = {
+    isVisible: true,
     options: Blocks.defaultOptions
 };
 
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..18dd44e5ea6c41204f706f665c88cfbb1c093b11
--- /dev/null
+++ b/src/containers/controls.jsx
@@ -0,0 +1,70 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import VM from 'scratch-vm';
+
+import ControlsComponent from '../components/controls/controls.jsx';
+
+class Controls extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleGreenFlagClick',
+            'handleStopAllClick',
+            'onProjectRunStart',
+            'onProjectRunStop'
+        ]);
+        this.state = {
+            projectRunning: false,
+            turbo: false
+        };
+    }
+    componentDidMount () {
+        this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
+        this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
+    }
+    componentWillUnmount () {
+        this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
+        this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
+    }
+    onProjectRunStart () {
+        this.setState({projectRunning: true});
+    }
+    onProjectRunStop () {
+        this.setState({projectRunning: false});
+    }
+    handleGreenFlagClick (e) {
+        e.preventDefault();
+        if (e.shiftKey) {
+            this.setState({turbo: !this.state.turbo});
+            this.props.vm.setTurboMode(!this.state.turbo);
+        } else {
+            this.props.vm.greenFlag();
+        }
+    }
+    handleStopAllClick (e) {
+        e.preventDefault();
+        this.props.vm.stopAll();
+    }
+    render () {
+        const {
+            vm, // eslint-disable-line no-unused-vars
+            ...props
+        } = this.props;
+        return (
+            <ControlsComponent
+                {...props}
+                active={this.state.projectRunning}
+                turbo={this.state.turbo}
+                onGreenFlagClick={this.handleGreenFlagClick}
+                onStopAllClick={this.handleStopAllClick}
+            />
+        );
+    }
+}
+
+Controls.propTypes = {
+    vm: PropTypes.instanceOf(VM)
+};
+
+export default Controls;
diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx
index b1f6acabf620c7b54f431f1f1e0e65821a580be5..a725b7e1ba6d198f69cc36da32c676ac9e350d12 100644
--- a/src/containers/green-flag.jsx
+++ b/src/containers/green-flag.jsx
@@ -11,24 +11,18 @@ class GreenFlag extends React.Component {
         super(props);
         bindAll(this, [
             'handleClick',
-            'handleKeyDown',
-            'handleKeyUp',
             'onProjectRunStart',
             'onProjectRunStop'
         ]);
-        this.state = {projectRunning: false, shiftKeyDown: false};
+        this.state = {projectRunning: false};
     }
     componentDidMount () {
         this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
         this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
-        document.addEventListener('keydown', this.handleKeyDown);
-        document.addEventListener('keyup', this.handleKeyUp);
     }
     componentWillUnmount () {
         this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
         this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
-        document.removeEventListener('keydown', this.handleKeyDown);
-        document.removeEventListener('keyup', this.handleKeyUp);
     }
     onProjectRunStart () {
         this.setState({projectRunning: true});
@@ -36,15 +30,9 @@ class GreenFlag extends React.Component {
     onProjectRunStop () {
         this.setState({projectRunning: false});
     }
-    handleKeyDown (e) {
-        this.setState({shiftKeyDown: e.shiftKey});
-    }
-    handleKeyUp (e) {
-        this.setState({shiftKeyDown: e.shiftKey});
-    }
     handleClick (e) {
         e.preventDefault();
-        if (this.state.shiftKeyDown) {
+        if (e.shiftKey) {
             this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode);
         } else {
             this.props.vm.greenFlag();
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 87f195aab83d5e1a70b7d33258babfeee7dd14a3..c41f7a9a97a8026ae96742354fcd6458a0135e08 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -1,3 +1,4 @@
+import AudioEngine from 'scratch-audio';
 import PropTypes from 'prop-types';
 import React from 'react';
 import VM from 'scratch-vm';
@@ -16,6 +17,8 @@ class GUI extends React.Component {
         this.state = {tabIndex: 0};
     }
     componentDidMount () {
+        this.audioEngine = new AudioEngine();
+        this.props.vm.attachAudioEngine(this.audioEngine);
         this.props.vm.loadProject(this.props.projectData);
         this.props.vm.setCompatibilityMode(true);
         this.props.vm.start();
@@ -33,6 +36,7 @@ class GUI extends React.Component {
     }
     render () {
         const {
+            children,
             projectData, // eslint-disable-line no-unused-vars
             vm,
             ...componentProps
@@ -43,7 +47,9 @@ class GUI extends React.Component {
                 vm={vm}
                 onTabSelect={this.handleTabSelect}
                 {...componentProps}
-            />
+            >
+                {children}
+            </GUIComponent>
         );
     }
 }
diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx
index fb98c070ccd616cfe0b88f46c8ee9a5e6e6e24c9..8fe3f97bd093365182507796afb5dc2a67f7445c 100644
--- a/src/containers/sound-library.jsx
+++ b/src/containers/sound-library.jsx
@@ -31,18 +31,19 @@ class SoundLibrary extends React.PureComponent {
         const idParts = md5ext.split('.');
         const md5 = idParts[0];
         const vm = this.props.vm;
-        vm.runtime.storage.load(vm.runtime.storage.AssetType.Sound, md5).then(soundAsset => {
-            const sound = {
-                md5: md5ext,
-                name: soundItem.name,
-                format: soundItem.format,
-                data: soundAsset.data
-            };
-            return this.audioEngine.decodeSound(sound);
-        })
-        .then(soundId => {
-            this.player.playSound(soundId);
-        });
+        vm.runtime.storage.load(vm.runtime.storage.AssetType.Sound, md5)
+            .then(soundAsset => {
+                const sound = {
+                    md5: md5ext,
+                    name: soundItem.name,
+                    format: soundItem.format,
+                    data: soundAsset.data
+                };
+                return this.audioEngine.decodeSound(sound);
+            })
+            .then(soundId => {
+                this.player.playSound(soundId);
+            });
     }
     handleItemMouseLeave () {
         this.player.stopAllSounds();
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index d9ab05f12966d701d898142b9f7a5a4855af9d15..1b5b2ec9cc4782fdb10a06e9099db31d7fcec64e 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -2,8 +2,8 @@ import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 import Renderer from 'scratch-render';
-import AudioEngine from 'scratch-audio';
 import VM from 'scratch-vm';
+import {getEventXY} from '../lib/touch-utils';
 
 import StageComponent from '../components/stage/stage.jsx';
 
@@ -37,8 +37,6 @@ class Stage extends React.Component {
         this.updateRect();
         this.renderer = new Renderer(this.canvas);
         this.props.vm.attachRenderer(this.renderer);
-        this.audioEngine = new AudioEngine();
-        this.props.vm.attachAudioEngine(this.audioEngine);
     }
     shouldComponentUpdate (nextProps) {
         return this.props.width !== nextProps.width || this.props.height !== nextProps.height;
@@ -50,12 +48,18 @@ class Stage extends React.Component {
     attachMouseEvents (canvas) {
         document.addEventListener('mousemove', this.onMouseMove);
         document.addEventListener('mouseup', this.onMouseUp);
+        document.addEventListener('touchmove', this.onMouseMove);
+        document.addEventListener('touchend', this.onMouseUp);
         canvas.addEventListener('mousedown', this.onMouseDown);
+        canvas.addEventListener('touchstart', this.onMouseDown);
     }
     detachMouseEvents (canvas) {
         document.removeEventListener('mousemove', this.onMouseMove);
         document.removeEventListener('mouseup', this.onMouseUp);
+        document.removeEventListener('touchmove', this.onMouseMove);
+        document.removeEventListener('touchend', this.onMouseUp);
         canvas.removeEventListener('mousedown', this.onMouseDown);
+        canvas.removeEventListener('touchstart', this.onMouseDown);
     }
     attachRectEvents () {
         window.addEventListener('resize', this.updateRect);
@@ -76,8 +80,9 @@ class Stage extends React.Component {
         ];
     }
     handleDoubleClick (e) {
+        const {x, y} = getEventXY(e);
         // Set editing target from cursor position, if clicking on a sprite.
-        const mousePosition = [e.clientX - this.rect.left, e.clientY - this.rect.top];
+        const mousePosition = [x - this.rect.left, y - this.rect.top];
         const drawableId = this.renderer.pick(mousePosition[0], mousePosition[1]);
         if (drawableId === null) return;
         const targetId = this.props.vm.getTargetIdForDrawableId(drawableId);
@@ -85,7 +90,8 @@ class Stage extends React.Component {
         this.props.vm.setEditingTarget(targetId);
     }
     onMouseMove (e) {
-        const mousePosition = [e.clientX - this.rect.left, e.clientY - this.rect.top];
+        const {x, y} = getEventXY(e);
+        const mousePosition = [x - this.rect.left, y - this.rect.top];
         if (this.state.mouseDownTimeoutId !== null) {
             this.cancelMouseDownTimeout();
             if (this.state.mouseDown && !this.state.isDragging) {
@@ -109,6 +115,7 @@ class Stage extends React.Component {
         this.props.vm.postIOData('mouse', coordinates);
     }
     onMouseUp (e) {
+        const {x, y} = getEventXY(e);
         this.cancelMouseDownTimeout();
         this.setState({
             mouseDown: false,
@@ -119,8 +126,8 @@ class Stage extends React.Component {
         } else {
             const data = {
                 isDown: false,
-                x: e.clientX - this.rect.left,
-                y: e.clientY - this.rect.top,
+                x: x - this.rect.left,
+                y: y - this.rect.top,
                 canvasWidth: this.rect.width,
                 canvasHeight: this.rect.height
             };
@@ -129,7 +136,8 @@ class Stage extends React.Component {
     }
     onMouseDown (e) {
         this.updateRect();
-        const mousePosition = [e.clientX - this.rect.left, e.clientY - this.rect.top];
+        const {x, y} = getEventXY(e);
+        const mousePosition = [x - this.rect.left, y - this.rect.top];
         this.setState({
             mouseDown: true,
             mouseDownPosition: mousePosition,
@@ -146,7 +154,9 @@ class Stage extends React.Component {
             canvasHeight: this.rect.height
         };
         this.props.vm.postIOData('mouse', data);
-        e.preventDefault();
+        if (e.preventDefault) {
+            e.preventDefault();
+        }
     }
     cancelMouseDownTimeout () {
         if (this.state.mouseDownTimeoutId !== null) {
diff --git a/src/css/colors.css b/src/css/colors.css
index 45258f13a7243bea9ddadb239654b3afe42473be..d524795cd5ece9550b0aee9767111e60d43c2f91 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -2,8 +2,11 @@ $ui-pane-border: #D9D9D9;
 $ui-pane-gray: #F9F9F9;
 $ui-background-blue: #e8edf1;
 
+$text-primary: #575e75;
+
 $motion-primary: #4C97FF;
 $motion-tertiary: #3373CC;
+$motion-transparent: hsla(215, 100%, 65%, 0.20);
 
 $red-primary: #FF661A;
 $red-tertiary: #E64D00;
@@ -11,4 +14,6 @@ $red-tertiary: #E64D00;
 $sound-primary: #CF63CF;
 $sound-tertiary: #A63FA6;
 
+$control-primary: #FFAB19;
+
 $form-border: #E9EEF2;
diff --git a/src/css/units.css b/src/css/units.css
index d951e3e1a874a4b34f22483d8d6cdc977d772e32..3de6174a2fe38b65148f7d99a5e459950e37160a 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -3,8 +3,8 @@ $space: 0.5rem;
 $sprites-per-row: 5;
 
 $menu-bar-height: 3rem;
-$sprite-info-height: 5.25rem; /* @todo: SpriteInfo isn't explicitly set to this height yet */
-$stage-menu-height: 3rem;
+$sprite-info-height: 6rem;
+$stage-menu-height: 2.75rem;
 
 $library-header-height: 4.375rem;
 
diff --git a/src/examples/blocks-only.css b/src/examples/blocks-only.css
new file mode 100644
index 0000000000000000000000000000000000000000..0f3ac07f8504fd62cbf72ad1245a0b9ca0946581
--- /dev/null
+++ b/src/examples/blocks-only.css
@@ -0,0 +1,6 @@
+.controls {
+    position: absolute;
+    z-index: 2;
+    top: 10px;
+    right: 15px;
+}
diff --git a/src/examples/blocks-only.jsx b/src/examples/blocks-only.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d5159e825105374827e818ce7009e57587e87de
--- /dev/null
+++ b/src/examples/blocks-only.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {connect} from 'react-redux';
+
+import AppStateHOC from '../lib/app-state-hoc.jsx';
+import Controls from '../containers/controls.jsx';
+import Blocks from '../containers/blocks.jsx';
+import GUI from '../containers/gui.jsx';
+import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
+
+import styles from './blocks-only.css';
+
+const mapStateToProps = state => ({vm: state.vm});
+
+const VMBlocks = connect(mapStateToProps)(Blocks);
+const VMControls = connect(mapStateToProps)(Controls);
+
+const BlocksOnly = props => (
+    <GUI {...props}>
+        <VMBlocks
+            grow={1}
+            options={{
+                media: `static/blocks-media/`
+            }}
+        />
+        <VMControls className={styles.controls} />
+    </GUI>
+);
+
+const App = AppStateHOC(ProjectLoaderHOC(BlocksOnly));
+
+const appTarget = document.createElement('div');
+document.body.appendChild(appTarget);
+
+ReactDOM.render(<App />, appTarget);
diff --git a/src/examples/player.css b/src/examples/player.css
new file mode 100644
index 0000000000000000000000000000000000000000..f5a3096c44d594f5bae77d04b8b9b5d2aaefa517
--- /dev/null
+++ b/src/examples/player.css
@@ -0,0 +1,4 @@
+body {
+    padding: 0;
+    margin: 0;
+}
diff --git a/src/examples/player.jsx b/src/examples/player.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ab50527cde1ae1d4be7788e7545397ff48b3700c
--- /dev/null
+++ b/src/examples/player.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {connect} from 'react-redux';
+
+import AppStateHOC from '../lib/app-state-hoc.jsx';
+import Controls from '../containers/controls.jsx';
+import Stage from '../containers/stage.jsx';
+import Box from '../components/box/box.jsx';
+import GUI from '../containers/gui.jsx';
+import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
+
+import './player.css';
+
+const mapStateToProps = state => ({vm: state.vm});
+
+const VMStage = connect(mapStateToProps)(Stage);
+const VMControls = connect(mapStateToProps)(Controls);
+
+class Player extends React.Component {
+    constructor (props) {
+        super(props);
+        this.handleResize = this.handleResize.bind(this);
+        this.state = this.getWindowSize();
+    }
+    componentDidMount () {
+        window.addEventListener('resize', this.handleResize);
+    }
+    componentWillUnmount () {
+        window.removeEventListener('resize', this.handleResize);
+    }
+    getWindowSize () {
+        return {
+            width: window.innerWidth,
+            height: window.innerHeight
+        };
+    }
+    handleResize () {
+        this.setState(this.getWindowSize());
+    }
+    render () {
+        let height = this.state.height - 40;
+        let width = height + (height / 3);
+        if (width > this.state.width) {
+            width = this.state.width;
+            height = width * .75;
+        }
+        return (
+            <GUI
+                {...this.props}
+                style={{
+                    margin: '0 auto'
+                }}
+                width={width}
+            >
+                <Box height={40}>
+                    <VMControls
+                        style={{
+                            marginRight: 10,
+                            height: 40
+                        }}
+                    />
+                </Box>
+                <VMStage
+                    height={height}
+                    width={width}
+                />
+            </GUI>
+        );
+    }
+}
+
+const App = AppStateHOC(ProjectLoaderHOC(Player));
+
+const appTarget = document.createElement('div');
+document.body.appendChild(appTarget);
+
+ReactDOM.render(<App />, appTarget);
diff --git a/src/index.jsx b/src/index.jsx
index 35c7610859e580e01d5e544d298bdb8f709b59bf..6d301f5168501f6b20252a6d0e034f509c5eea77 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,79 +1,16 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import {Provider} from 'react-redux';
-import {createStore, applyMiddleware, compose} from 'redux';
-import throttle from 'redux-throttle';
-import {intlInitialState, IntlProvider} from './reducers/intl.js';
 
+import AppStateHOC from './lib/app-state-hoc.jsx';
 import GUI from './containers/gui.jsx';
-import log from './lib/log';
-import ProjectLoader from './lib/project-loader';
-import reducer from './reducers/gui';
+import ProjectLoaderHOC from './lib/project-loader-hoc.jsx';
 
 import styles from './index.css';
 
-class App extends React.Component {
-    constructor (props) {
-        super(props);
-        this.fetchProjectId = this.fetchProjectId.bind(this);
-        this.updateProject = this.updateProject.bind(this);
-        this.state = {
-            projectId: null,
-            projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
-        };
-    }
-    componentDidMount () {
-        window.addEventListener('hashchange', this.updateProject);
-        this.updateProject();
-    }
-    componentWillUnmount () {
-        window.removeEventListener('hashchange', this.updateProject);
-    }
-    fetchProjectId () {
-        return window.location.hash.substring(1);
-    }
-    updateProject () {
-        const projectId = this.fetchProjectId();
-        if (projectId !== this.state.projectId) {
-            if (projectId.length < 1) {
-                return this.setState({
-                    projectId: projectId,
-                    projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
-                });
-            }
-            ProjectLoader.load(projectId, (err, body) => {
-                if (err) return log.error(err);
-                this.setState({projectData: body});
-            });
-            this.setState({projectId: projectId});
-        }
-    }
-    render () {
-        if (this.state.projectData === null) return null;
-        return (
-            <GUI
-                projectData={this.state.projectData}
-            />
-        );
-    }
-}
+const App = AppStateHOC(ProjectLoaderHOC(GUI));
 
 const appTarget = document.createElement('div');
 appTarget.className = styles.app;
 document.body.appendChild(appTarget);
 
-const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
-const enhancer = composeEnhancers(
-    applyMiddleware(
-        throttle(300, {leading: true, trailing: true})
-    )
-);
-const store = createStore(reducer, intlInitialState, enhancer);
-
-ReactDOM.render((
-    <Provider store={store}>
-        <IntlProvider>
-            <App />
-        </IntlProvider>
-    </Provider>
-), appTarget);
+ReactDOM.render(<App />, appTarget);
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0861f05291f258f8cf625947723da763f9f22d8c
--- /dev/null
+++ b/src/lib/app-state-hoc.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import {Provider} from 'react-redux';
+import {createStore, applyMiddleware, compose} from 'redux';
+import throttle from 'redux-throttle';
+
+import {intlInitialState, IntlProvider} from '../reducers/intl.js';
+import reducer from '../reducers/gui';
+
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
+const enhancer = composeEnhancers(
+    applyMiddleware(
+        throttle(300, {leading: true, trailing: true})
+    )
+);
+const store = createStore(reducer, intlInitialState, enhancer);
+
+/*
+ * Higher Order Component to provide redux state
+ * @param {React.Component} WrappedComponent - component to provide state for
+ * @returns {React.Component} component with redux and intl state provided
+ */
+const AppStateHOC = function (WrappedComponent) {
+    const AppStateWrapper = ({...props}) => (
+        <Provider store={store}>
+            <IntlProvider>
+                <WrappedComponent {...props} />
+            </IntlProvider>
+        </Provider>
+    );
+    return AppStateWrapper;
+};
+
+export default AppStateHOC;
diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5468e7a09b54c292c9e107183fff80550fb0c29d
--- /dev/null
+++ b/src/lib/project-loader-hoc.jsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import xhr from 'xhr';
+
+import log from './log';
+import emptyProject from './empty-project.json';
+
+class ProjectLoaderConstructor {
+    get DEFAULT_PROJECT_DATA () {
+        return emptyProject;
+    }
+
+    load (id, callback) {
+        callback = callback || (err => log.error(err));
+        xhr({
+            uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/`
+        }, (err, res, body) => {
+            if (err) return callback(err);
+            callback(null, body);
+        });
+    }
+}
+
+const ProjectLoader = new ProjectLoaderConstructor();
+
+/* Higher Order Component to provide behavior for loading projects by id from
+ * the window's hash (#this part in the url)
+ * @param {React.Component} WrappedComponent component to receive projectData prop
+ * @returns {React.Component} component with project loading behavior
+ */
+const ProjectLoaderHOC = function (WrappedComponent) {
+    class ProjectLoaderComponent extends React.Component {
+        constructor (props) {
+            super(props);
+            this.fetchProjectId = this.fetchProjectId.bind(this);
+            this.updateProject = this.updateProject.bind(this);
+            this.state = {
+                projectId: null,
+                projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
+            };
+        }
+        componentDidMount () {
+            window.addEventListener('hashchange', this.updateProject);
+            this.updateProject();
+        }
+        componentWillUnmount () {
+            window.removeEventListener('hashchange', this.updateProject);
+        }
+        fetchProjectId () {
+            return window.location.hash.substring(1);
+        }
+        updateProject () {
+            const projectId = this.fetchProjectId();
+            if (projectId !== this.state.projectId) {
+                if (projectId.length < 1) {
+                    return this.setState({
+                        projectId: projectId,
+                        projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
+                    });
+                }
+                ProjectLoader.load(projectId, (err, body) => {
+                    if (err) return log.error(err);
+                    this.setState({projectData: body});
+                });
+                this.setState({projectId: projectId});
+            }
+        }
+        render () {
+            if (!this.state.projectData) return null;
+            return (
+                <WrappedComponent
+                    projectData={this.state.projectData}
+                    {...this.props}
+                />
+            );
+        }
+    }
+
+    return ProjectLoaderComponent;
+};
+
+
+export {
+    ProjectLoaderHOC as default,
+    ProjectLoader
+};
diff --git a/src/lib/project-loader.js b/src/lib/project-loader.js
deleted file mode 100644
index 05060732065e890b13e99e0cb9e4b90fe47dff31..0000000000000000000000000000000000000000
--- a/src/lib/project-loader.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import xhr from 'xhr';
-
-import log from './log';
-import emptyProject from './empty-project.json';
-
-class ProjectLoader {
-    constructor () {
-        this.DEFAULT_PROJECT_DATA = ProjectLoader.DEFAULT_PROJECT_DATA;
-    }
-    load (id, callback) {
-        callback = callback || (err => log.error(err));
-        xhr({
-            uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/`
-        }, (err, res, body) => {
-            if (err) return callback(err);
-            callback(null, body);
-        });
-    }
-}
-
-ProjectLoader.DEFAULT_PROJECT_DATA = emptyProject;
-
-export default new ProjectLoader();
diff --git a/src/lib/touch-utils.js b/src/lib/touch-utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..07c638d8d4653cfe1df99cad5a80e20aeb38bf87
--- /dev/null
+++ b/src/lib/touch-utils.js
@@ -0,0 +1,12 @@
+const getEventXY = e => {
+    if (e.touches && e.touches[0]) {
+        return {x: e.touches[0].clientX, y: e.touches[0].clientY};
+    } else if (e.changedTouches && e.changedTouches[0]) {
+        return {x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY};
+    }
+    return {x: e.clientX, y: e.clientY};
+};
+
+export {
+    getEventXY
+};
diff --git a/test/.eslintrc.js b/test/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..ea7ef84f15c71d2b379ba0e22b700e82cd8a8893
--- /dev/null
+++ b/test/.eslintrc.js
@@ -0,0 +1,10 @@
+module.exports = {
+    extends: ['scratch/react', 'scratch/es6'],
+    env: {
+        browser: true,
+        jest: true
+    },
+    rules: {
+        'react/prop-types': 0
+    }
+};
diff --git a/test/__mocks__/audio-buffer-player.js b/test/__mocks__/audio-buffer-player.js
index c36092be339f166b2376a5340c1e82abed6c9ca9..dabf72bc37d2ba6553530ada7dc4e9851f4f53a7 100644
--- a/test/__mocks__/audio-buffer-player.js
+++ b/test/__mocks__/audio-buffer-player.js
@@ -1,4 +1,3 @@
-/* eslint-env jest */
 export default class MockAudioBufferPlayer {
     constructor (samples, sampleRate) {
         this.samples = samples;
diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js
index 95c1bd8585ba3acd54b50e12666aac7b3084fa9c..291438f85fae419280e77e0c5b7dc79cba08b57c 100644
--- a/test/__mocks__/audio-effects.js
+++ b/test/__mocks__/audio-effects.js
@@ -1,4 +1,3 @@
-/* eslint-env jest */
 export default class MockAudioEffects {
     static get effectTypes () { // @todo can this be imported from the real file?
         return {
diff --git a/test/helpers/intl-helpers.js b/test/helpers/intl-helpers.jsx
similarity index 54%
rename from test/helpers/intl-helpers.js
rename to test/helpers/intl-helpers.jsx
index d658aeae0981adc70d8de4f57d3ec196a06f2df1..8c9a057b4e54a2d0cabd9c9fb93b8959d2bb65a0 100644
--- a/test/helpers/intl-helpers.js
+++ b/test/helpers/intl-helpers.jsx
@@ -10,33 +10,27 @@ import {mount, shallow} from 'enzyme';
 const intlProvider = new IntlProvider({locale: 'en'}, {});
 const {intl} = intlProvider.getChildContext();
 
-const nodeWithIntlProp = node => {
-    return React.cloneElement(node, {intl});
-};
+const nodeWithIntlProp = node => React.cloneElement(node, {intl});
 
-const shallowWithIntl = (node, {context} = {}) => {
-    return shallow(
-        nodeWithIntlProp(node),
-        {
-            context: Object.assign({}, context, {intl})
-        }
-    );
-};
+const shallowWithIntl = (node, {context} = {}) => shallow(
+    nodeWithIntlProp(node),
+    {
+        context: Object.assign({}, context, {intl})
+    }
+);
 
-const mountWithIntl = (node, {context, childContextTypes} = {}) => {
-    return mount(
-        nodeWithIntlProp(node),
-        {
-            context: Object.assign({}, context, {intl}),
-            childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
-        }
-    );
-};
+const mountWithIntl = (node, {context, childContextTypes} = {}) => mount(
+    nodeWithIntlProp(node),
+    {
+        context: Object.assign({}, context, {intl}),
+        childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
+    }
+);
 
 // react-test-renderer component for use with snapshot testing
-const componentWithIntl = (children, props = {locale: 'en'}) => {
-    return renderer.create(<IntlProvider {...props}>{children}</IntlProvider>);
-};
+const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create(
+    <IntlProvider {...props}>{children}</IntlProvider>
+);
 
 export {
     componentWithIntl,
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
new file mode 100644
index 0000000000000000000000000000000000000000..390740c3c2e9b7e8b3225388fe8141e55435d3ce
--- /dev/null
+++ b/test/helpers/selenium-helper.js
@@ -0,0 +1,65 @@
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef
+
+import bindAll from 'lodash.bindall';
+import webdriver from 'selenium-webdriver';
+
+const {By, until} = webdriver;
+
+class SeleniumHelper {
+    constructor () {
+        bindAll(this, [
+            'clickText',
+            'clickButton',
+            'clickXpath',
+            'findByXpath',
+            'getDriver',
+            'getLogs'
+        ]);
+    }
+
+    getDriver () {
+        this.driver = new webdriver.Builder()
+            .forBrowser('chrome')
+            .build();
+        return this.driver;
+    }
+
+    findByXpath (xpath) {
+        return this.driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000));
+    }
+
+    clickXpath (xpath) {
+        return this.findByXpath(xpath).then(el => el.click());
+    }
+
+    clickText (text) {
+        return this.clickXpath(`//*[contains(text(), '${text}')]`);
+    }
+
+    clickButton (text) {
+        return this.clickXpath(`//button[contains(text(), '${text}')]`);
+    }
+
+    getLogs (whitelist) {
+        return this.driver.manage()
+            .logs()
+            .get('browser')
+            .then(entries => entries.filter(entry => {
+                const message = entry.message;
+                for (let i = 0; i < whitelist.length; i++) {
+                    if (message.indexOf(whitelist[i]) !== -1) {
+                        // eslint-disable-next-line no-console
+                        console.warn(`Ignoring whitelisted error: ${whitelist[i]}`);
+                        return false;
+                    } else if (entry.level !== 'SEVERE') {
+                        // eslint-disable-next-line no-console
+                        console.warn(`Ignoring non-SEVERE entry: ${message}`);
+                        return false;
+                    }
+                }
+                return true;
+            }));
+    }
+}
+
+export default SeleniumHelper;
diff --git a/test/helpers/selenium-helpers.js b/test/helpers/selenium-helpers.js
deleted file mode 100644
index 909f3a1553eaca14269f494216a1f55140aff87e..0000000000000000000000000000000000000000
--- a/test/helpers/selenium-helpers.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/* eslint-env jest */
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef
-
-import webdriver from 'selenium-webdriver';
-
-const {By, until} = webdriver;
-
-const driver = new webdriver.Builder()
-    .forBrowser('chrome')
-    .build();
-
-const findByXpath = (xpath) => {
-    return driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000));
-};
-
-const clickXpath = (xpath) => {
-    return findByXpath(xpath).then(el => el.click());
-};
-
-const clickText = (text) => {
-    return clickXpath(`//*[contains(text(), '${text}')]`);
-};
-
-const clickButton = (text) => {
-    return clickXpath(`//button[contains(text(), '${text}')]`);
-};
-
-const getLogs = (whitelist) => {
-    return driver.manage()
-        .logs()
-        .get('browser')
-        .then((entries) => {
-            return entries.filter((entry) => {
-                const message = entry.message;
-                for (let i = 0; i < whitelist.length; i++) {
-                    if (message.indexOf(whitelist[i]) !== -1) {
-                        // eslint-disable-next-line no-console
-                        console.warn('Ignoring whitelisted error: ' + whitelist[i]);
-                        return false;
-                    } else if (entry.level !== 'SEVERE') {
-                        // eslint-disable-next-line no-console
-                        console.warn('Ignoring non-SEVERE entry: ' + message);
-                        return false;
-                    }
-                }
-                return true;
-            });
-        });
-};
-
-export {
-    clickText,
-    clickButton,
-    clickXpath,
-    driver,
-    findByXpath,
-    getLogs
-};
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..3299fb27c532df3838a82dd5cfa04528aa3c287b
--- /dev/null
+++ b/test/integration/examples.test.js
@@ -0,0 +1,87 @@
+/* globals Promise */
+
+import path from 'path';
+import SeleniumHelper from '../helpers/selenium-helper';
+
+const {
+    clickButton,
+    clickText,
+    clickXpath,
+    findByXpath,
+    getDriver,
+    getLogs
+} = new SeleniumHelper();
+
+const errorWhitelist = [
+    'The play() request was interrupted by a call to pause()'
+];
+
+let driver;
+
+describe('player example', () => {
+    const uri = path.resolve(__dirname, '../../build/player.html');
+
+    beforeAll(() => {
+        driver = getDriver();
+    });
+
+    afterAll(async () => {
+        await driver.quit();
+    });
+
+    test('Load a project by ID', async () => {
+        const projectId = '96708228';
+        await driver.get(`file://${uri}#${projectId}`);
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Go"]');
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Stop"]');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+});
+
+describe('blocks example', () => {
+    const uri = path.resolve(__dirname, '../../build/blocks-only.html');
+
+    beforeAll(() => {
+        driver = getDriver();
+    });
+
+    afterAll(async () => {
+        await driver.quit();
+    });
+
+    test('Load a project by ID', async () => {
+        const projectId = '96708228';
+        await driver.get(`file://${uri}#${projectId}`);
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Go"]');
+        await new Promise(resolve => setTimeout(resolve, 2000));
+        await clickXpath('//img[@title="Stop"]');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+
+    test('Change categories', async () => {
+        await driver.get(`file://${uri}`);
+        await clickText('Looks');
+        await clickText('Sound');
+        await clickText('Pen');
+        await clickText('Events');
+        await clickText('Control');
+        await clickText('Sensing');
+        await clickText('Operators');
+        await clickText('Data');
+        await clickText('Create variable...');
+        let el = await findByXpath("//input[@placeholder='']");
+        await el.sendKeys('score');
+        await clickButton('OK');
+        await clickText('Create variable...');
+        el = await findByXpath("//input[@placeholder='']");
+        await el.sendKeys('second variable');
+        await clickButton('OK');
+        const logs = await getLogs(errorWhitelist);
+        await expect(logs).toEqual([]);
+    });
+});
diff --git a/test/integration/test.js b/test/integration/test.js
index 7e3e60a651a6e7d64f06ad56c561058449432f20..39e8c6faec5a2451e08fb0133a0dd34462738fb4 100644
--- a/test/integration/test.js
+++ b/test/integration/test.js
@@ -1,15 +1,14 @@
-/* eslint-env jest */
-/* globals Promise */
-
 import path from 'path';
-import {
+import SeleniumHelper from '../helpers/selenium-helper';
+
+const {
     clickText,
     clickButton,
     clickXpath,
-    driver,
     findByXpath,
+    getDriver,
     getLogs
-} from '../helpers/selenium-helpers';
+} = new SeleniumHelper();
 
 const uri = path.resolve(__dirname, '../../build/index.html');
 
@@ -17,13 +16,19 @@ const errorWhitelist = [
     'The play() request was interrupted by a call to pause()'
 ];
 
+let driver;
+
 describe('costumes, sounds and variables', () => {
+    beforeAll(() => {
+        driver = getDriver();
+    });
+
     afterAll(async () => {
         await driver.quit();
     });
 
     test('Adding a costume', async () => {
-        await driver.get('file://' + uri);
+        await driver.get(`file://${uri}`);
         await clickText('Costumes');
         await clickText('Add Costume');
         const el = await findByXpath("//input[@placeholder='what are you looking for?']");
@@ -36,7 +41,7 @@ describe('costumes, sounds and variables', () => {
     });
 
     test('Adding a sound', async () => {
-        await driver.get('file://' + uri);
+        await driver.get(`file://${uri}`);
         await clickText('Sounds');
         await clickText('Add Sound');
         const el = await findByXpath("//input[@placeholder='what are you looking for?']");
@@ -62,7 +67,7 @@ describe('costumes, sounds and variables', () => {
 
     test('Load a project by ID', async () => {
         const projectId = '96708228';
-        await driver.get('file://' + uri + '#' + projectId);
+        await driver.get(`file://${uri}#${projectId}`);
         await new Promise(resolve => setTimeout(resolve, 2000));
         await clickXpath('//img[@title="Go"]');
         await new Promise(resolve => setTimeout(resolve, 2000));
@@ -72,7 +77,7 @@ describe('costumes, sounds and variables', () => {
     });
 
     test('Creating variables', async () => {
-        await driver.get('file://' + uri);
+        await driver.get(`file://${uri}`);
         await clickText('Blocks');
         await clickText('Data');
         await clickText('Create variable...');
diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
index 644d330d0e5a099f59cfe4c527d93b5aa5913535..6e863223f1d8753ce674382f29db0153edf82b65 100644
--- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
+++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
@@ -111,6 +111,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         <div
           className=""
           onMouseDown={[Function]}
+          onTouchStart={[Function]}
           style={
             Object {
               "alignContent": undefined,
@@ -231,6 +232,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         <div
           className=""
           onMouseDown={[Function]}
+          onTouchStart={[Function]}
           style={
             Object {
               "alignContent": undefined,
diff --git a/test/unit/components/button.test.jsx b/test/unit/components/button.test.jsx
index 6f9b7447c4d48a0ce8a82f45fcf3ae6f394fc519..5445891ab1eabb9e9db44a0fc81366622474eb90 100644
--- a/test/unit/components/button.test.jsx
+++ b/test/unit/components/button.test.jsx
@@ -1,14 +1,13 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
+import React from 'react';
 import {shallow} from 'enzyme';
-import ButtonComponent from '../../../src/components/button/button'; // eslint-disable-line no-unused-vars
+import ButtonComponent from '../../../src/components/button/button';
 import renderer from 'react-test-renderer';
 
 describe('ButtonComponent', () => {
     test('matches snapshot', () => {
         const onClick = jest.fn();
         const component = renderer.create(
-            <ButtonComponent onClick={onClick}/>
+            <ButtonComponent onClick={onClick} />
         );
         expect(component.toJSON()).toMatchSnapshot();
     });
@@ -16,7 +15,7 @@ describe('ButtonComponent', () => {
     test('triggers callback when clicked', () => {
         const onClick = jest.fn();
         const componentShallowWrapper = shallow(
-            <ButtonComponent onClick={onClick}/>
+            <ButtonComponent onClick={onClick} />
         );
         componentShallowWrapper.simulate('click');
         expect(onClick).toHaveBeenCalled();
diff --git a/test/unit/components/controls.test.jsx b/test/unit/components/controls.test.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..fcf22208d410c9815af9705b0941b40883fa8673
--- /dev/null
+++ b/test/unit/components/controls.test.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
+import Controls from '../../../src/components/controls/controls';
+import TurboMode from '../../../src/components/turbo-mode/turbo-mode';
+
+describe('Controls component', () => {
+    const defaultProps = () => ({
+        active: false,
+        greenFlagTitle: 'Go',
+        onGreenFlagClick: jest.fn(),
+        onStopAllClick: jest.fn(),
+        stopAllTitle: 'Stop',
+        turbo: false
+    });
+
+    test('shows turbo mode when in turbo mode', () => {
+        const component = shallowWithIntl(
+            <Controls
+                {...defaultProps()}
+            />
+        );
+        expect(component.find(TurboMode).exists()).toEqual(false);
+        component.setProps({turbo: true});
+        expect(component.find(TurboMode).exists()).toEqual(true);
+    });
+
+    test('triggers the right callbacks when clicked', () => {
+        const props = defaultProps();
+        const component = shallowWithIntl(
+            <Controls
+                {...props}
+            />
+        );
+        component.find('[title="Go"]').simulate('click');
+        expect(props.onGreenFlagClick).toHaveBeenCalled();
+
+        component.find('[title="Stop"]').simulate('click');
+        expect(props.onStopAllClick).toHaveBeenCalled();
+    });
+});
diff --git a/test/unit/components/icon-button.test.jsx b/test/unit/components/icon-button.test.jsx
index fe2498a1a36b160759f2a29e1a3c98a5f320fb93..269dfad76fd30fec0a580baa21eca45302142edb 100644
--- a/test/unit/components/icon-button.test.jsx
+++ b/test/unit/components/icon-button.test.jsx
@@ -1,7 +1,6 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
+import React from 'react';
 import {shallow} from 'enzyme';
-import IconButton from '../../../src/components/icon-button/icon-button'; // eslint-disable-line no-unused-vars
+import IconButton from '../../../src/components/icon-button/icon-button';
 import renderer from 'react-test-renderer';
 
 describe('IconButtonComponent', () => {
diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx
index 6c12f473060ed5b7167fe2b353563345b349c92d..387a53c1a73359e3870a288bf1d34adf83fb9985 100644
--- a/test/unit/components/sound-editor.test.jsx
+++ b/test/unit/components/sound-editor.test.jsx
@@ -1,7 +1,6 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
-import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers';
-import SoundEditor from '../../../src/components/sound-editor/sound-editor'; // eslint-disable-line no-unused-vars
+import React from 'react';
+import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx';
+import SoundEditor from '../../../src/components/sound-editor/sound-editor';
 
 describe('Sound Editor Component', () => {
     let props;
@@ -38,31 +37,55 @@ describe('Sound Editor Component', () => {
     });
 
     test('trim button appears when trims are null', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={null} trimEnd={null} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                trimEnd={null}
+                trimStart={null}
+            />
+        );
         wrapper.find('button[title="Trim"]').simulate('click');
         expect(props.onActivateTrim).toHaveBeenCalled();
     });
 
     test('save button appears when trims are not null', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={0.25} trimEnd={0.75} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                trimEnd={0.75}
+                trimStart={0.25}
+            />
+        );
         wrapper.find('button[title="Save"]').simulate('click');
         expect(props.onActivateTrim).toHaveBeenCalled();
     });
 
     test('play button appears when playhead is null', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} playhead={null} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                playhead={null}
+            />
+        );
         wrapper.find('button[title="Play"]').simulate('click');
         expect(props.onPlay).toHaveBeenCalled();
     });
 
     test('stop button appears when playhead is not null', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} playhead={0.5} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                playhead={0.5}
+            />
+        );
         wrapper.find('button[title="Stop"]').simulate('click');
         expect(props.onStop).toHaveBeenCalled();
     });
 
     test('submitting name calls the callback', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor {...props} />
+        );
         wrapper.find('input')
             .simulate('change', {target: {value: 'hello'}})
             .simulate('blur');
@@ -70,7 +93,9 @@ describe('Sound Editor Component', () => {
     });
 
     test('effect buttons call the correct callbacks', () => {
-        const wrapper = mountWithIntl(<SoundEditor {...props} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor {...props} />
+        );
 
         wrapper.find('[children="Reverse"]').simulate('click');
         expect(props.onReverse).toHaveBeenCalled();
@@ -95,17 +120,35 @@ describe('Sound Editor Component', () => {
     });
 
     test('undo and redo buttons can be disabled by canUndo/canRedo', () => {
-        let wrapper = mountWithIntl(<SoundEditor {...props} canUndo={true} canRedo={false} />);
+        let wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                canUndo
+                canRedo={false}
+            />
+        );
         expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(false);
         expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(true);
 
-        wrapper = mountWithIntl(<SoundEditor {...props} canUndo={false} canRedo={true} />);
+        wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                canRedo
+                canUndo={false}
+            />
+        );
         expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(true);
         expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(false);
     });
 
     test.skip('undo/redo buttons call the correct callback', () => {
-        let wrapper = mountWithIntl(<SoundEditor {...props} canUndo={true} canRedo={true} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                {...props}
+                canRedo
+                canUndo
+            />
+        );
         wrapper.find('button[title="Undo"]').simulate('click');
         expect(props.onUndo).toHaveBeenCalled();
 
diff --git a/test/unit/components/sprite-selector-item.test.jsx b/test/unit/components/sprite-selector-item.test.jsx
index fe0b1de5238b5076e9938da8303dfb714023212c..e1d7ac7bb73ae23fb10ff018035a097ef0137e06 100644
--- a/test/unit/components/sprite-selector-item.test.jsx
+++ b/test/unit/components/sprite-selector-item.test.jsx
@@ -1,10 +1,8 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
-import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers';
-// eslint-disable-next-line no-unused-vars
+import React from 'react';
+import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx';
 import SpriteSelectorItemComponent from '../../../src/components/sprite-selector-item/sprite-selector-item';
 import CostumeCanvas from '../../../src/components/costume-canvas/costume-canvas';
-import CloseButton from '../../../src/components/close-button/close-button'; // eslint-disable-line no-unused-vars
+import CloseButton from '../../../src/components/close-button/close-button';
 
 describe('SpriteSelectorItemComponent', () => {
     let className;
@@ -16,13 +14,16 @@ describe('SpriteSelectorItemComponent', () => {
 
     // Wrap this in a function so it gets test specific states and can be reused.
     const getComponent = function () {
-        return <SpriteSelectorItemComponent
-            className={className}
-            costumeURL={costumeURL}
-            name={name}
-            onClick={onClick}
-            onDeleteButtonClick={onDeleteButtonClick}
-            selected={selected}/>;
+        return (
+            <SpriteSelectorItemComponent
+                className={className}
+                costumeURL={costumeURL}
+                name={name}
+                selected={selected}
+                onClick={onClick}
+                onDeleteButtonClick={onDeleteButtonClick}
+            />
+        );
     };
 
     beforeEach(() => {
diff --git a/test/unit/containers/green-flag.test.jsx b/test/unit/containers/green-flag.test.jsx
index 24c3ae9c546e82d4b89476992bbcf1f159b0413f..4744ebaabd147318afdf430596f53e243b950700 100644
--- a/test/unit/containers/green-flag.test.jsx
+++ b/test/unit/containers/green-flag.test.jsx
@@ -1,7 +1,6 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
+import React from 'react';
 import {shallow} from 'enzyme';
-import GreenFlag from '../../../src/containers/green-flag'; // eslint-disable-line no-unused-vars
+import GreenFlag from '../../../src/containers/green-flag';
 import renderer from 'react-test-renderer';
 import VM from 'scratch-vm';
 
@@ -13,14 +12,20 @@ describe('GreenFlag Container', () => {
 
     test('renders active state', () => {
         const component = renderer.create(
-            <GreenFlag active={true} vm={vm}/>
+            <GreenFlag
+                active
+                vm={vm}
+            />
         );
         expect(component.toJSON()).toMatchSnapshot();
     });
 
     test('renders inactive state', () => {
         const component = renderer.create(
-            <GreenFlag active={false} vm={vm}/>
+            <GreenFlag
+                active={false}
+                vm={vm}
+            />
         );
         expect(component.toJSON()).toMatchSnapshot();
     });
@@ -28,7 +33,11 @@ describe('GreenFlag Container', () => {
     test('triggers onClick when active', () => {
         const onClick = jest.fn();
         const componentShallowWrapper = shallow(
-            <GreenFlag active={true} onClick={onClick} vm={vm}/>
+            <GreenFlag
+                active
+                vm={vm}
+                onClick={onClick}
+            />
         );
         componentShallowWrapper.simulate('click');
         expect(onClick).toHaveBeenCalled();
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index 527d3ed16c78b4cdc9d37b00cbfac724b302701b..187908176cb87db2573b71cb0c5e0b7111685976 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -1,12 +1,10 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
-import {mountWithIntl} from '../../helpers/intl-helpers';
+import React from 'react';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
 import configureStore from 'redux-mock-store';
 import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js';
 import mockAudioEffects from '../../__mocks__/audio-effects.js';
 
-import SoundEditor from '../../../src/containers/sound-editor'; // eslint-disable-line no-unused-vars
-// eslint-disable-next-line no-unused-vars
+import SoundEditor from '../../../src/containers/sound-editor';
 import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor';
 
 jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer);
@@ -17,7 +15,7 @@ describe('Sound Editor Container', () => {
     let store;
     let soundIndex;
     let soundBuffer;
-    let samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef
+    const samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef
     let vm;
 
     beforeEach(() => {
@@ -40,7 +38,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('should pass the correct data to the component from the store', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const componentProps = wrapper.find(SoundEditorComponent).props();
         // Data retreived and processed by the `connect` with the store
         expect(componentProps.name).toEqual('first name');
@@ -54,7 +57,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it plays when clicked and stops when clicked again', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         // Ensure rendering doesn't start playing any sounds
         expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]);
@@ -73,7 +81,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it sets the component props for trimming and submits to the vm', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
 
         component.props().onActivateTrim();
@@ -87,14 +100,24 @@ describe('Sound Editor Container', () => {
     });
 
     test('it submits name changes to the vm', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onChangeName('hello');
         expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
     });
 
-    test('it handles an effect by submitting the result and playing', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+    test('it handles an effect by submitting the result and playing', done => {
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onReverse(); // Could be any of the effects, just testing the end result
         mockAudioEffects.instance._finishProcessing(soundBuffer);
@@ -103,7 +126,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles reverse effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onReverse();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.REVERSE);
@@ -111,7 +139,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles louder effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onLouder();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.LOUDER);
@@ -119,7 +152,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles softer effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onSofter();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SOFTER);
@@ -127,7 +165,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles faster effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onFaster();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.FASTER);
@@ -135,7 +178,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles slower effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onSlower();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SLOWER);
@@ -143,7 +191,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles echo effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onEcho();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ECHO);
@@ -151,7 +204,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('it handles robot effect correctly', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onRobot();
         expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ROBOT);
@@ -159,7 +217,12 @@ describe('Sound Editor Container', () => {
     });
 
     test('undo/redo functionality', () => {
-        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(
+            <SoundEditor
+                soundIndex={soundIndex}
+                store={store}
+            />
+        );
         const component = wrapper.find(SoundEditorComponent);
         // Undo and redo should be disabled initially
         expect(component.prop('canUndo')).toEqual(false);
diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx
index 1efd40cb7cc05a26f9521dee06ad3935225fd7e8..2a371a1a0dfe763c32737ee832a9a3009e57b0e7 100644
--- a/test/unit/containers/sprite-selector-item.test.jsx
+++ b/test/unit/containers/sprite-selector-item.test.jsx
@@ -1,11 +1,10 @@
-/* eslint-env jest */
-import React from 'react'; // eslint-disable-line no-unused-vars
-import {mountWithIntl} from '../../helpers/intl-helpers';
+import React from 'react';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
 import configureStore from 'redux-mock-store';
-import {Provider} from 'react-redux'; // eslint-disable-line no-unused-vars
+import {Provider} from 'react-redux';
 
-import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; // eslint-disable-line no-unused-vars
-import CloseButton from '../../../src/components/close-button/close-button'; // eslint-disable-line no-unused-vars
+import SpriteSelectorItem from '../../../src/containers/sprite-selector-item';
+import CloseButton from '../../../src/components/close-button/close-button';
 
 describe('SpriteSelectorItem Container', () => {
     const mockStore = configureStore();
@@ -19,14 +18,19 @@ describe('SpriteSelectorItem Container', () => {
     let store;
     // Wrap this in a function so it gets test specific states and can be reused.
     const getContainer = function () {
-        return <Provider store={store}><SpriteSelectorItem
-            className={className}
-            costumeURL={costumeURL}
-            id={id}
-            name={name}
-            onClick={onClick}
-            onDeleteButtonClick={onDeleteButtonClick}
-            selected={selected}/></Provider>;
+        return (
+            <Provider store={store}>
+                <SpriteSelectorItem
+                    className={className}
+                    costumeURL={costumeURL}
+                    id={id}
+                    name={name}
+                    selected={selected}
+                    onClick={onClick}
+                    onDeleteButtonClick={onDeleteButtonClick}
+                />
+            </Provider>
+        );
     };
 
     beforeEach(() => {
diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js
index e30a2e6c14d40528fddf8b5dd2f69ebc93bec5f0..d1057de53777355a9d9f9a7dfbf4babf95c3e63d 100644
--- a/test/unit/util/audio-effects.test.js
+++ b/test/unit/util/audio-effects.test.js
@@ -1,5 +1,4 @@
-/* eslint-env jest */
-/* global AudioNode AudioContext WebAudioTestAPI */
+/* global WebAudioTestAPI */
 import 'web-audio-test-api';
 WebAudioTestAPI.setState({
     'OfflineAudioContext#startRendering': 'promise'
@@ -11,8 +10,8 @@ import EchoEffect from '../../../src/lib/audio/effects/echo-effect';
 import VolumeEffect from '../../../src/lib/audio/effects/volume-effect';
 
 describe('Audio Effects manager', () => {
-    let audioContext = new AudioContext();
-    let audioBuffer = audioContext.createBuffer(1, 400, 44100);
+    const audioContext = new AudioContext();
+    const audioBuffer = audioContext.createBuffer(1, 400, 44100);
 
     test('changes buffer length and playback rate for faster effect', () => {
         const audioEffects = new AudioEffects(audioBuffer, 'faster');
diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js
index 743a5b1c492894ce420b1f6bb66fa977eb2f4522..24ddcd4559567dce0fd1677a11a809c365233699 100644
--- a/test/unit/util/audio-util.test.js
+++ b/test/unit/util/audio-util.test.js
@@ -1,4 +1,3 @@
-/* eslint-env jest */
 import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util';
 
 describe('computeRMS', () => {
diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..964894c4369441e328e6e365f97631d3736f15b6
--- /dev/null
+++ b/test/unit/util/project-loader-hoc.test.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import ProjectLoaderHOC, {ProjectLoader} from '../../../src/lib/project-loader-hoc.jsx';
+import {mount} from 'enzyme';
+
+describe('ProjectLoaderHOC', () => {
+    test('when there is no project data, it renders null', () => {
+        const Component = ({projectData}) => <div>{projectData}</div>;
+        const WrappedComponent = ProjectLoaderHOC(Component);
+        window.location.hash = '#winning';
+        ProjectLoader.load = jest.fn((id, cb) => cb(null, null));
+        const mounted = mount(<WrappedComponent />);
+        ProjectLoader.load.mockRestore();
+        window.location.hash = '';
+        expect(mounted.find('div').exists()).toEqual(false);
+    });
+
+    test('when there is no hash, it loads the default project', () => {
+        const Component = ({projectData}) => <div>{projectData}</div>;
+        const WrappedComponent = ProjectLoaderHOC(Component);
+        window.location.hash = '';
+        const mounted = mount(<WrappedComponent />);
+        expect(mounted.find('div').text()).toEqual(JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA));
+    });
+
+    test('when there is a hash, it tries to load that project', () => {
+        const Component = ({projectData}) => <div>{projectData}</div>;
+        const WrappedComponent = ProjectLoaderHOC(Component);
+        window.location.hash = '#winning';
+        ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
+        const mounted = mount(<WrappedComponent />);
+        mounted.update();
+        ProjectLoader.load.mockRestore();
+        window.location.hash = '';
+        expect(mounted
+            .find('div')
+            .text()
+        ).toEqual('winning');
+    });
+
+    test('when hash change happens, the project data state is changed', () => {
+        const Component = ({projectData}) => <div>{projectData}</div>;
+        const WrappedComponent = ProjectLoaderHOC(Component);
+        window.location.hash = '';
+        const mounted = mount(<WrappedComponent />);
+        const before = mounted.find('div').text();
+        ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
+        window.location.hash = `#winning`;
+        mounted.node.updateProject();
+        expect(mounted.find('div').text()).not.toEqual(before);
+    });
+});
diff --git a/webpack.config.js b/webpack.config.js
index 44595ff2cd505fc28a3e6bc4666732a802c0c86d..f39fc81a9f7cf0f487d01fd6077e8d5a548de77c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -19,7 +19,9 @@ module.exports = {
     devtool: 'cheap-module-source-map',
     entry: {
         lib: ['react', 'react-dom'],
-        gui: './src/index.jsx'
+        gui: './src/index.jsx',
+        blocksonly: './src/examples/blocks-only.jsx',
+        player: './src/examples/player.jsx'
     },
     output: {
         path: path.resolve(__dirname, 'build'),
@@ -78,9 +80,22 @@ module.exports = {
             filename: 'lib.min.js'
         }),
         new HtmlWebpackPlugin({
+            chunks: ['lib', 'gui'],
             template: 'src/index.ejs',
             title: 'Scratch 3.0 GUI'
         }),
+        new HtmlWebpackPlugin({
+            chunks: ['lib', 'blocksonly'],
+            template: 'src/index.ejs',
+            filename: 'blocks-only.html',
+            title: 'Scratch 3.0 GUI: Blocks Only Example'
+        }),
+        new HtmlWebpackPlugin({
+            chunks: ['lib', 'player'],
+            template: 'src/index.ejs',
+            filename: 'player.html',
+            title: 'Scratch 3.0 GUI: Player Example'
+        }),
         new CopyWebpackPlugin([{
             from: 'node_modules/scratch-blocks/media',
             to: 'static/blocks-media'