diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index d3b2f9d4480de7260e6a15d2650077e1486dd0bd..21cef41e9a6c66b2072b2625d6ad3f1a828f7154 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -34,7 +34,7 @@ const messages = defineMessages({
     addSpriteFromFile: {
         id: 'gui.spriteSelector.addSpriteFromFile',
         description: 'Button to add a sprite in the target pane from file',
-        defaultMessage: 'Coming Soon'
+        defaultMessage: 'Upload'
     }
 });
 
@@ -51,12 +51,15 @@ const SpriteSelectorComponent = function (props) {
         onChangeSpriteY,
         onDeleteSprite,
         onDuplicateSprite,
+        onFileUploadClick,
         onNewSpriteClick,
-        onSurpriseSpriteClick,
         onPaintSpriteClick,
         onSelectSprite,
+        onSpriteUpload,
+        onSurpriseSpriteClick,
         raised,
         selectedId,
+        spriteFileInput,
         sprites,
         ...componentProps
     } = props;
@@ -121,7 +124,11 @@ const SpriteSelectorComponent = function (props) {
                 moreButtons={[
                     {
                         title: intl.formatMessage(messages.addSpriteFromFile),
-                        img: fileUploadIcon
+                        img: fileUploadIcon,
+                        onClick: onFileUploadClick,
+                        fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2', // TODO add sprite 3
+                        fileChange: onSpriteUpload,
+                        fileInput: spriteFileInput
                     }, {
                         title: intl.formatMessage(messages.addSpriteFromSurprise),
                         img: surpriseIcon,
@@ -154,12 +161,15 @@ SpriteSelectorComponent.propTypes = {
     onChangeSpriteY: PropTypes.func,
     onDeleteSprite: PropTypes.func,
     onDuplicateSprite: PropTypes.func,
+    onFileUploadClick: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onPaintSpriteClick: PropTypes.func,
     onSelectSprite: PropTypes.func,
+    onSpriteUpload: PropTypes.func,
     onSurpriseSpriteClick: PropTypes.func,
     raised: PropTypes.bool,
     selectedId: PropTypes.string,
+    spriteFileInput: PropTypes.func,
     sprites: PropTypes.shape({
         id: PropTypes.shape({
             costume: PropTypes.shape({
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 4be489f6948e718c7fe8c2cecb90c44db388726a..fb08f3b413c8da8105a40309571c4c5a95cb383b 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -17,6 +17,7 @@ import styles from './target-pane.css';
  */
 const TargetPane = ({
     editingTarget,
+    fileInputRef,
     hoveredTarget,
     spriteLibraryVisible,
     onChangeSpriteDirection,
@@ -27,11 +28,13 @@ const TargetPane = ({
     onChangeSpriteY,
     onDeleteSprite,
     onDuplicateSprite,
+    onFileUploadClick,
     onNewSpriteClick,
-    onSurpriseSpriteClick,
     onPaintSpriteClick,
     onRequestCloseSpriteLibrary,
     onSelectSprite,
+    onSpriteUpload,
+    onSurpriseSpriteClick,
     raiseSprites,
     stage,
     sprites,
@@ -48,6 +51,7 @@ const TargetPane = ({
             hoveredTarget={hoveredTarget}
             raised={raiseSprites}
             selectedId={editingTarget}
+            spriteFileInput={fileInputRef}
             sprites={sprites}
             onChangeSpriteDirection={onChangeSpriteDirection}
             onChangeSpriteName={onChangeSpriteName}
@@ -57,9 +61,11 @@ const TargetPane = ({
             onChangeSpriteY={onChangeSpriteY}
             onDeleteSprite={onDeleteSprite}
             onDuplicateSprite={onDuplicateSprite}
+            onFileUploadClick={onFileUploadClick}
             onNewSpriteClick={onNewSpriteClick}
             onPaintSpriteClick={onPaintSpriteClick}
             onSelectSprite={onSelectSprite}
+            onSpriteUpload={onSpriteUpload}
             onSurpriseSpriteClick={onSurpriseSpriteClick}
         />
         <div className={styles.stageSelectorWrapper}>
@@ -108,6 +114,7 @@ const spriteShape = PropTypes.shape({
 TargetPane.propTypes = {
     editingTarget: PropTypes.string,
     extensionLibraryVisible: PropTypes.bool,
+    fileInputRef: PropTypes.func,
     hoveredTarget: PropTypes.shape({
         hoveredSprite: PropTypes.string,
         receivedBlocks: PropTypes.bool
@@ -120,11 +127,13 @@ TargetPane.propTypes = {
     onChangeSpriteY: PropTypes.func,
     onDeleteSprite: PropTypes.func,
     onDuplicateSprite: PropTypes.func,
+    onFileUploadClick: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onPaintSpriteClick: PropTypes.func,
     onRequestCloseExtensionLibrary: PropTypes.func,
     onRequestCloseSpriteLibrary: PropTypes.func,
     onSelectSprite: PropTypes.func,
+    onSpriteUpload: PropTypes.func,
     onSurpriseSpriteClick: PropTypes.func,
     raiseSprites: PropTypes.bool,
     spriteLibraryVisible: PropTypes.bool,
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index 97d6dffaa31e4c89e9f09a02d0e7acda5bb8fe76..535e4b270a19111bdea51874511672cb28244074 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -13,6 +13,7 @@ import {setReceivedBlocks} from '../reducers/hovered-target';
 
 import TargetPaneComponent from '../components/target-pane/target-pane.jsx';
 import spriteLibraryContent from '../lib/libraries/sprites.json';
+import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
 
 class TargetPane extends React.Component {
     constructor (props) {
@@ -27,9 +28,13 @@ class TargetPane extends React.Component {
             'handleChangeSpriteY',
             'handleDeleteSprite',
             'handleDuplicateSprite',
+            'handleNewSprite',
             'handleSelectSprite',
             'handleSurpriseSpriteClick',
-            'handlePaintSpriteClick'
+            'handlePaintSpriteClick',
+            'handleFileUploadClick',
+            'handleSpriteUpload',
+            'setFileInput'
         ]);
     }
     componentDidMount () {
@@ -80,6 +85,21 @@ class TargetPane extends React.Component {
             });
         }
     }
+    handleNewSprite (spriteJSONString) {
+        this.props.vm.addSprite(spriteJSONString); // TODO change all instances of addSprite2 to addSprite?
+    }
+    handleFileUploadClick () {
+        this.fileInput.click();
+    }
+    handleSpriteUpload (e) {
+        const storage = this.props.vm.runtime.storage;
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            spriteUpload(buffer, fileType, fileName, storage, this.handleNewSprite);
+        });
+    }
+    setFileInput (input) {
+        this.fileInput = input;
+    }
     handleBlockDragEnd (blocks) {
         if (this.props.hoveredTarget.sprite && this.props.hoveredTarget.sprite !== this.props.editingTarget) {
             this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite);
@@ -95,6 +115,7 @@ class TargetPane extends React.Component {
         return (
             <TargetPaneComponent
                 {...componentProps}
+                fileInputRef={this.setFileInput}
                 onChangeSpriteDirection={this.handleChangeSpriteDirection}
                 onChangeSpriteName={this.handleChangeSpriteName}
                 onChangeSpriteSize={this.handleChangeSpriteSize}
@@ -103,8 +124,10 @@ class TargetPane extends React.Component {
                 onChangeSpriteY={this.handleChangeSpriteY}
                 onDeleteSprite={this.handleDeleteSprite}
                 onDuplicateSprite={this.handleDuplicateSprite}
+                onFileUploadClick={this.handleFileUploadClick}
                 onPaintSpriteClick={this.handlePaintSpriteClick}
                 onSelectSprite={this.handleSelectSprite}
+                onSpriteUpload={this.handleSpriteUpload}
                 onSurpriseSpriteClick={this.handleSurpriseSpriteClick}
             />
         );
diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js
index ca4ce8773c113b92ef9fcfb460daeda609242482..cfe54784b329774b0e92d44b9477b3c16e808ee9 100644
--- a/src/lib/file-uploader.js
+++ b/src/lib/file-uploader.js
@@ -47,6 +47,7 @@ const handleFileUpload = function (fileInput, onload) {
  * @property {string} dataFormat The data format of this asset, typically
  * the extension to be used for that particular asset, e.g. 'svg' for vector images
  * @property {string} md5 The md5 hash of the asset data, followed by '.'' and dataFormat
+ * @property {string} The md5 hash of the asset data // TODO remove duplication....
  */
 
 /**
@@ -71,7 +72,8 @@ const cacheAsset = function (storage, fileName, assetType, dataFormat, data) {
     return {
         name: fileName,
         dataFormat: dataFormat,
-        md5: `${md5}.${dataFormat}`
+        md5: `${md5}.${dataFormat}`,
+        assetId: md5
     };
 };
 
@@ -171,8 +173,59 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun
     handleSound(vmSound);
 };
 
+const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite) {
+    switch (fileType) {
+    case '': { // We think this is a .sprite2 or .sprite3 file
+        handleSprite(new Uint8Array(fileData));
+        // TODO VM addSprite function should handle
+        // buffers directly and get unpacked in scratch-parser
+        return;
+    }
+    case 'image/svg+xml':
+    case 'image/png':
+    case 'imag/jpeg': {
+        // Make a sprite from an image by making it a costume first
+
+        // let costume = null;
+        costumeUpload(fileData, fileType, `${spriteName}-costume1`, storage, (vmCostume => {
+            const newSprite = {
+                targets: [{
+                    name: spriteName,
+                    isStage: false,
+                    x: 0, // what should we put here...
+                    y: 0,
+                    visible: true,
+                    size: 100,
+                    rotationStyle: 'all around',
+                    direction: 90,
+                    draggable: true,
+                    currentCostume: 0,
+                    blocks: {},
+                    variables: {},
+                    lists: {},
+                    broadcasts: {},
+                    costumes: [vmCostume],
+                    sounds: [] // TODO are all of these necessary?
+                }]
+            };
+            // TODO probably just want sprite upload to handle this object directly
+            handleSprite(JSON.stringify(newSprite));
+        }));
+        return;
+    }
+    // case 'archive/zip': { // sprite2 / sprite3
+    //     handleSprite(JSON.stringify(fileData.toString('utf-8')));
+    //     return;
+    // }
+    default: {
+        return;
+    }
+    }
+};
+
 export {
     handleFileUpload,
     costumeUpload,
-    soundUpload
+    soundUpload,
+    spriteUpload
 };