diff --git a/.eslintignore b/.eslintignore
index 3675648e816b3221576be2393fd3496bb99dad70..93c033154fbdf0e4cb6ef0dd9c31570e2b6cd475 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
 node_modules/*
 build/*
+dist/*
diff --git a/.gitignore b/.gitignore
index 8f98cc55bfece970be899bd47c999bfd9ae70369..0b8a0db40e62622a65702b0b042181de5136286e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ npm-*
 
 # Build
 /build
+/dist
 /.opt-in
 
 # generated translation files
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000000000000000000000000000000000000..5d69158021a19c3cf2923c0630dfd7509f0dd03e
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,16 @@
+# Mac OS
+.DS_Store
+
+# NPM
+/node_modules
+npm-*
+
+# Testing
+/.nyc_output
+/coverage
+
+# Build
+/.opt-in
+
+# generated translation files
+/translations
diff --git a/.travis.yml b/.travis.yml
index 59e1d15ba92569c05cd41357603636935a74fff3..55dcc33c0bdc6003ceff02e9d360902a915d87e2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,12 +5,14 @@ addons:
     chrome: stable
 node_js:
 - 8
+env:
+  - NODE_ENV=production
 cache:
   directories:
   - node_modules
 install:
-- npm install
-- npm update
+- npm --production=false install
+- npm --production=false update
 before_deploy:
 - npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S)
 - git config --global user.email $(git log --pretty=format:"%ae" -n1)
diff --git a/package.json b/package.json
index f6d243d46cdccc9473727f729fe398806e1e2a03..e6654a757641eb7560515e153598545395e3b02d 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,14 @@
   "name": "scratch-gui",
   "version": "0.1.0",
   "description": "GraphicaL User Interface for creating and running Scratch 3.0 projects",
-  "main": "./src/index.js",
+  "main": "./dist/scratch-gui.js",
   "scripts": {
     "build": "npm run clean && webpack --progress --colors --bail",
-    "clean": "rimraf ./build && mkdirp build",
+    "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
     "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
     "i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/ ./translations/",
     "start": "webpack-dev-server",
-    "test": "npm run test:lint && npm run test:unit && NODE_ENV=production npm run build && npm run test:integration",
+    "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration",
     "test:integration": "jest --runInBand test[\\\\/]integration",
     "test:lint": "eslint . --ext .js,.jsx",
     "test:unit": "jest test[\\\\/]unit",
@@ -37,7 +37,7 @@
     "babel-preset-es2015": "^6.22.0",
     "babel-preset-react": "^6.22.0",
     "buffer-loader": "0.0.1",
-    "chromedriver": "2.37.0",
+    "chromedriver": "2.38.0",
     "classnames": "2.2.5",
     "copy-webpack-plugin": "^4.3.0",
     "css-loader": "^0.28.7",
@@ -89,12 +89,13 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.1523977528",
-    "scratch-blocks": "0.1.0-prerelease.1523542868",
+    "scratch-blocks": "0.1.0-prerelease.1524267558",
     "scratch-l10n": "2.0.20180108132626",
-    "scratch-paint": "0.2.0-prerelease.20180410152401",
-    "scratch-render": "0.1.0-prerelease.1523453612",
+    "scratch-paint": "0.2.0-prerelease.20180423143518",
+    "scratch-render": "0.1.0-prerelease.20180423214437",
+    "scratch-svg-renderer": "0.1.0-prerelease.20180423193917",
     "scratch-storage": "0.4.0",
-    "scratch-vm": "0.1.0-prerelease.1523642820",
+    "scratch-vm": "0.1.0-prerelease.1524520946",
     "selenium-webdriver": "3.6.0",
     "startaudiocontext": "1.2.1",
     "style-loader": "^0.20.0",
diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx
index 0fa100a35fdfc4e8e054be395cae0a29231dcefe..09af880cf78fd69168cbe2e8ac52c10169044e7e 100644
--- a/src/components/action-menu/action-menu.jsx
+++ b/src/components/action-menu/action-menu.jsx
@@ -137,12 +137,12 @@ class ActionMenu extends React.Component {
                 <div className={styles.moreButtonsOuter}>
                     <div className={styles.moreButtons}>
                         {(moreButtons || []).map(({img, title, onClick: handleClick,
-                            fileAccept, fileChange, fileInput}) => {
+                            fileAccept, fileChange, fileInput}, keyId) => {
                             const isComingSoon = !handleClick;
                             const hasFileInput = fileInput;
                             const tooltipId = title;
                             return (
-                                <div key={tooltipId}>
+                                <div key={`${tooltipId}-${keyId}`}>
                                     <button
                                         aria-label={title}
                                         className={classNames(styles.button, styles.moreButton, {
diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx
index 9052d5f57075796500df46e8f172cca3405e0f8f..94a9d8545e78e5df72a8ef29c7a6d5114149deb1 100644
--- a/src/components/browser-modal/browser-modal.jsx
+++ b/src/components/browser-modal/browser-modal.jsx
@@ -43,34 +43,18 @@ const BrowserModal = ({intl, ...props}) => (
                     className={styles.backButton}
                     onClick={props.onBack}
                 >
-                    <FormattedMessage
-                        defaultMessage="Back"
-                        description="Label for button go back when browser is unsupported"
-                        id="gui.unsupportedBrowser.back"
-                    />
+                    Back
                 </button>
 
             </Box>
             <div className={styles.faqLinkText}>
-                <FormattedMessage
-                    defaultMessage="To learn more, go to the {previewFaqLink}."
-                    description="Scratch 3.0 FAQ description"
-                    id="gui.unsupportedBrowser.previewfaq"
-                    values={{
-                        previewFaqLink: (
-                            <a
-                                className={styles.faqLink}
-                                href="//scratch.mit.edu/preview-faq"
-                            >
-                                <FormattedMessage
-                                    defaultMessage="preview FAQ"
-                                    description="link to Scratch 3.0 FAQ page"
-                                    id="gui.unsupportedBrowser.previewfaqlink"
-                                />
-                            </a>
-                        )
-                    }}
-                />
+                To learn more, go to the {' '}
+                <a
+                    className={styles.faqLink}
+                    href="//scratch.mit.edu/preview-faq"
+                >
+                    preview FAQ
+                </a>.
             </div>
         </Box>
     </ReactModal>
diff --git a/src/components/button/button.css b/src/components/button/button.css
index 5e69aabd477b6507ef774c9249586cdeb29b8198..9ce746c52140b73be1ba889fae9c028ab7cfe2bb 100644
--- a/src/components/button/button.css
+++ b/src/components/button/button.css
@@ -7,6 +7,7 @@
     align-items: center;
     padding-left: .75rem;
     padding-right: .75rem;
+    user-select: none;
 }
 
 .icon {
diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx
index 1d348ba24d7bfc3ce60a02b244e36e2f9f2b3373..a4c9ffe48b9ab6eba6965e8b018243a43936c151 100644
--- a/src/components/button/button.jsx
+++ b/src/components/button/button.jsx
@@ -48,7 +48,7 @@ ButtonComponent.propTypes = {
     disabled: PropTypes.bool,
     iconClassName: PropTypes.string,
     iconSrc: PropTypes.string,
-    onClick: PropTypes.func.isRequired
+    onClick: PropTypes.func
 };
 
 export default ButtonComponent;
diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx
index befac229f5b0a5da58f3c1e1b25b6d9d64c79357..98cf78d026015117e61e6814f1b4c44e0c1c6ff2 100644
--- a/src/components/close-button/close-button.jsx
+++ b/src/components/close-button/close-button.jsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
 
 import styles from './close-button.css';
 import closeIcon from './icon--close.svg';
-import backIcon from './icon--back.svg';
+import backIcon from '../../lib/assets/icon--back.svg';
 
 const CloseButton = props => (
     <div
diff --git a/src/components/divider/divider.css b/src/components/divider/divider.css
new file mode 100644
index 0000000000000000000000000000000000000000..47ce3a29671a4e117603d16704cef578347654cd
--- /dev/null
+++ b/src/components/divider/divider.css
@@ -0,0 +1,5 @@
+@import "../../css/colors.css";
+
+.divider {
+    border-right: 1px dashed $ui-black-transparent;
+}
diff --git a/src/components/divider/divider.jsx b/src/components/divider/divider.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..38975fd0bada5b29ac1bba296a7c1925d88badf3
--- /dev/null
+++ b/src/components/divider/divider.jsx
@@ -0,0 +1,15 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import styles from './divider.css';
+
+const Divider = ({className}) => (
+    <div className={classNames(styles.divider, className)} />
+);
+
+Divider.propTypes = {
+    className: PropTypes.string
+};
+
+export default Divider;
diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css
index a80fd4159a562a4850e88b1bdbdc78892558289f..b67e60795c29a100c7e67982b8dafeb8bb46c28b 100644
--- a/src/components/filter/filter.css
+++ b/src/components/filter/filter.css
@@ -5,27 +5,28 @@
     display: flex;
     flex-direction: row;
     align-items: center;
-     /*
-        Should we use width 100% instead, for cases when component's
-        parent is not a flexbox container?
-    */
     flex-grow: 1;
 
-    padding: 0.3rem 0.5rem;
-    background: $ui-black-transparent;
-    border: 1px solid $ui-white-transparent;
+    background: $ui-white;
     border-radius: 10rem;
     user-select: none;
-}
+    height: $library-filter-bar-height;
 
-.filter:hover {
-    background: $ui-black-transparent;
+    position: relative;
 }
 
 .filter-icon {
-    height: 0.9rem;
-    width: 0.9rem;
-    margin: 0 0.5rem 0 0.75rem;
+    position: absolute;
+    top: 0;
+    left: 0;
+
+    height: 1rem;
+    width: 1rem;
+    margin: 0.75rem 0.75rem 0.75rem 1rem;
+}
+
+.filter:focus-within {
+    box-shadow: 0 0 0 .25rem $motion-transparent;
 }
 
 /*
@@ -33,6 +34,9 @@
 */
 .x-icon-wrapper {
     opacity: 0;
+    position: absolute;
+    top: 0;
+    right: 0;
 
     display: flex;
     justify-content: center;
@@ -41,7 +45,7 @@
     overflow: hidden;  /* Mask the icon animation */
     height: 1.25rem;
     width: 1.25rem;
-    margin: 0 0.25rem 0 0.5rem; /* @todo: move to parent to make component*/
+    margin: 0.625rem;
 
     border-radius: 50%;
     pointer-events: none;
@@ -83,21 +87,22 @@
 
 .filter-input {
     flex-grow: 1;
-    line-height: 1.25rem;
+    height: $library-filter-bar-height;
     background-color: transparent;
     -webkit-appearance: none;
     outline: none;
     border: 0;
-    color: white;
+    color: $text-primary;
     font-size: 0.75rem;
     letter-spacing: 0.15px;
     cursor: text;
+    padding: .625rem 2rem .625rem 3rem;
 }
 
 .filter-input::placeholder {
-    opacity: 0.75;
+    opacity: .5;
     padding: 0 0 0 0.25rem;
-    color: white;
-    font-size: 0.75rem;
+    color: $text-primary;
+    font-size: 0.875rem;
     letter-spacing: 0.15px;
 }
diff --git a/src/components/filter/filter.jsx b/src/components/filter/filter.jsx
index 6ccf4fdd49eb64e5537957436d0e6597a1216258..ab58e99f712408e723edbfc239aa5884dee05c15 100644
--- a/src/components/filter/filter.jsx
+++ b/src/components/filter/filter.jsx
@@ -8,15 +8,16 @@ import styles from './filter.css';
 
 const FilterComponent = props => {
     const {
+        className,
         onChange,
         onClear,
         placeholderText,
-        filterQuery
+        filterQuery,
+        inputClassName
     } = props;
     return (
         <div
-            className={classNames({
-                [styles.filter]: true,
+            className={classNames(className, styles.filter, {
                 [styles.isActive]: filterQuery.length > 0
             })}
         >
@@ -25,7 +26,7 @@ const FilterComponent = props => {
                 src={filterIcon}
             />
             <input
-                className={styles.filterInput}
+                className={classNames(styles.filterInput, inputClassName)}
                 placeholder={placeholderText}
                 type="text"
                 value={filterQuery}
@@ -45,12 +46,14 @@ const FilterComponent = props => {
 };
 
 FilterComponent.propTypes = {
+    className: PropTypes.string,
     filterQuery: PropTypes.string,
+    inputClassName: PropTypes.string,
     onChange: PropTypes.func,
     onClear: PropTypes.func,
     placeholderText: PropTypes.string
 };
 FilterComponent.defaultProps = {
-    placeholderText: 'what are you looking for?'
+    placeholderText: 'Search'
 };
 export default FilterComponent;
diff --git a/src/components/filter/icon--filter.svg b/src/components/filter/icon--filter.svg
index 2a0c75fcb9ffc6aa604b66b0ce6320994e2b086a..400bd6234eb1589ae3f5c9f9c64568d702c62bd4 100644
Binary files a/src/components/filter/icon--filter.svg and b/src/components/filter/icon--filter.svg differ
diff --git a/src/components/filter/icon--x.svg b/src/components/filter/icon--x.svg
index e78a2a8e24a8fe526004cd1ec4e1a2622dec76d2..88b52e87007a6bb84576180dfd0364f548271753 100644
Binary files a/src/components/filter/icon--x.svg and b/src/components/filter/icon--x.svg differ
diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx
index e4779d8c6b561d3df41575af07979fdc805d8fbb..befe4797681a1097ad47c05f360129b045658b65 100644
--- a/src/components/language-selector/language-selector.jsx
+++ b/src/components/language-selector/language-selector.jsx
@@ -53,7 +53,7 @@ const LanguageSelector = ({
 LanguageSelector.propTypes = {
     currentLocale: PropTypes.string,
     onChange: PropTypes.func,
-    open: PropTypes.boolean
+    open: PropTypes.bool
 };
 
 export default LanguageSelector;
diff --git a/src/components/library/library.css b/src/components/library/library.css
index 8eb86eec0982892ce49b8b1fbd343af7524f6285..5a2b6e1f0f47a9f58822298f791453d860ed5042 100644
--- a/src/components/library/library.css
+++ b/src/components/library/library.css
@@ -1,23 +1,6 @@
 @import "../../css/units.css";
 @import "../../css/colors.css";
 
-.modal-content {
-    position: absolute;
-
-    display: flex;
-    height: 100%;
-    width: 100%;
-
-    overflow-y: auto;
-    -webkit-overflow-scrolling: 'touch';
-    user-select: none;
-
-    /* Default modal resets */
-    margin: 0;
-    border: none;
-    border-radius: 0;
-}
-
 .library-scroll-grid {
     display: flex;
     justify-content: flex-start;
@@ -29,3 +12,49 @@
     height: calc(100% - $library-header-height);
     padding: 0.5rem;
 }
+
+.library-scroll-grid.withFilterBar {
+    height: calc(100% - $library-header-height - $library-filter-bar-height - 2rem);
+}
+
+.filter-bar {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+    height: calc($library-filter-bar-height + 2rem); /* padding */
+    background-color: $motion-transparent;
+    padding: 0 1rem;
+    font-size: .875rem;
+}
+
+.filter-bar-item {
+    margin-right: .75rem;
+}
+
+.filter {
+    flex-grow: 0;
+}
+
+.filter-input {
+    width: 11.5rem;
+    transition: .2s;
+}
+
+.filter-input:focus,
+.filter-input:not([value=""]) {
+    width: 18.75rem;
+}
+
+.divider {
+    transform: scaleY(1.39);
+    height: $library-filter-bar-height;
+}
+
+.tag-wrapper {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    height: $library-filter-bar-height;
+    overflow: hidden;
+}
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index 1592e99c51bcf0226a72cd7438fc4089a919bee6..e9996d4f2ea4716f35c650e713605aa803a5ff31 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -1,12 +1,19 @@
+import classNames from 'classnames';
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 
 import LibraryItem from '../library-item/library-item.jsx';
-import ModalComponent from '../modal/modal.jsx';
+import Modal from '../../containers/modal.jsx';
+import Divider from '../divider/divider.jsx';
+import Filter from '../filter/filter.jsx';
+import TagButton from '../../containers/tag-button.jsx';
 
 import styles from './library.css';
 
+const ALL_TAG_TITLE = 'All';
+const tagListPrefix = [{title: ALL_TAG_TITLE}];
+
 class LibraryComponent extends React.Component {
     constructor (props) {
         super(props);
@@ -17,11 +24,13 @@ class LibraryComponent extends React.Component {
             'handleFocus',
             'handleMouseEnter',
             'handleMouseLeave',
-            'handleSelect'
+            'handleSelect',
+            'handleTagClick'
         ]);
         this.state = {
             selectedItem: null,
-            filterQuery: ''
+            filterQuery: '',
+            selectedTag: ALL_TAG_TITLE.toLowerCase()
         };
     }
     handleBlur (id) {
@@ -34,6 +43,12 @@ class LibraryComponent extends React.Component {
         this.props.onRequestClose();
         this.props.onItemSelected(this.getFilteredData()[id]);
     }
+    handleTagClick (tag) {
+        this.setState({
+            filterQuery: '',
+            selectedTag: tag.toLowerCase()
+        });
+    }
     handleMouseEnter (id) {
         if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]);
     }
@@ -41,26 +56,82 @@ class LibraryComponent extends React.Component {
         if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]);
     }
     handleFilterChange (event) {
-        this.setState({filterQuery: event.target.value});
+        this.setState({
+            filterQuery: event.target.value,
+            selectedTag: ALL_TAG_TITLE.toLowerCase()
+        });
     }
     handleFilterClear () {
         this.setState({filterQuery: ''});
     }
     getFilteredData () {
-        return this.props.data.filter(dataItem =>
-            dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1);
+        if (this.state.selectedTag === 'all') {
+            if (!this.state.filterQuery) return this.props.data;
+            return this.props.data.filter(dataItem => (
+                (dataItem.tags || [])
+                    // Second argument to map sets `this`
+                    .map(String.prototype.toLowerCase.call, String.prototype.toLowerCase)
+                    .concat(dataItem.name.toLowerCase())
+                    .join('\n') // unlikely to partially match newlines
+                    .indexOf(this.state.filterQuery.toLowerCase()) !== -1
+            ));
+        }
+        return this.props.data.filter(dataItem => (
+            dataItem.tags &&
+            dataItem.tags
+                .map(String.prototype.toLowerCase.call, String.prototype.toLowerCase)
+                .indexOf(this.state.selectedTag) !== -1
+        ));
     }
     render () {
         return (
-            <ModalComponent
-                className={styles.modalContent}
+            <Modal
+                fullScreen
                 contentLabel={this.props.title}
-                filterQuery={this.state.filterQuery}
-                onFilterChange={this.handleFilterChange}
-                onFilterClear={this.handleFilterClear}
+                id={this.props.id}
                 onRequestClose={this.props.onRequestClose}
             >
-                <div className={styles.libraryScrollGrid}>
+                {(this.props.filterable || this.props.tags) && (
+                    <div className={styles.filterBar}>
+                        {this.props.filterable && (
+                            <Filter
+                                className={classNames(
+                                    styles.filterBarItem,
+                                    styles.filter
+                                )}
+                                filterQuery={this.state.filterQuery}
+                                inputClassName={styles.filterInput}
+                                onChange={this.handleFilterChange}
+                                onClear={this.handleFilterClear}
+                            />
+                        )}
+                        {this.props.filterable && this.props.tags && (
+                            <Divider className={classNames(styles.filterBarItem, styles.divider)} />
+                        )}
+                        {this.props.tags &&
+                            <div className={styles.tagWrapper}>
+                                {tagListPrefix.concat(this.props.tags).map((tagProps, id) => (
+                                    <TagButton
+                                        active={this.state.selectedTag === tagProps.title.toLowerCase()}
+                                        className={classNames(
+                                            styles.filterBarItem,
+                                            styles.tagButton,
+                                            tagProps.className
+                                        )}
+                                        key={`tag-button-${id}`}
+                                        onClick={this.handleTagClick}
+                                        {...tagProps}
+                                    />
+                                ))}
+                            </div>
+                        }
+                    </div>
+                )}
+                <div
+                    className={classNames(styles.libraryScrollGrid, {
+                        [styles.withFilterBar]: this.props.filterable || this.props.tags
+                    })}
+                >
                     {this.getFilteredData().map((dataItem, index) => {
                         const scratchURL = dataItem.md5 ?
                             `https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
@@ -83,7 +154,7 @@ class LibraryComponent extends React.Component {
                         );
                     })}
                 </div>
-            </ModalComponent>
+            </Modal>
         );
     }
 }
@@ -100,11 +171,18 @@ LibraryComponent.propTypes = {
         })
         /* eslint-enable react/no-unused-prop-types, lines-around-comment */
     ),
+    filterable: PropTypes.bool,
+    id: PropTypes.string.isRequired,
     onItemMouseEnter: PropTypes.func,
     onItemMouseLeave: PropTypes.func,
     onItemSelected: PropTypes.func,
     onRequestClose: PropTypes.func,
+    tags: PropTypes.arrayOf(PropTypes.shape(TagButton.propTypes)),
     title: PropTypes.string.isRequired
 };
 
+LibraryComponent.defaultProps = {
+    filterable: true
+};
+
 export default LibraryComponent;
diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css
index 4805e302dd0f24cecae0d4257d4a371fd71bafe6..1a26960a4581daece08d44e088419878e0644723 100644
--- a/src/components/menu-bar/menu-bar.css
+++ b/src/components/menu-bar/menu-bar.css
@@ -81,7 +81,6 @@
 }
 
 .divider {
-    border-right: 1px dashed $ui-black-transparent;
     margin: 0 .5rem;
     height: 34px;
 }
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index 633b8130dd600fa91abf2b366971750162dc37f6..f647b24df534aa6e96ee65917dc59b5b056a0cb7 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -7,6 +7,7 @@ import React from 'react';
 import Box from '../box/box.jsx';
 import Button from '../button/button.jsx';
 import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
+import Divider from '../divider/divider.jsx';
 import LanguageSelector from '../../containers/language-selector.jsx';
 import ProjectLoader from '../../containers/project-loader.jsx';
 import Menu from '../../containers/menu.jsx';
@@ -181,7 +182,7 @@ const MenuBar = props => (
                     </MenuBarMenu>
                 </div>
             </div>
-            <div className={classNames(styles.divider)} />
+            <Divider className={classNames(styles.divider)} />
             <div className={classNames(styles.menuBarItem)}>
                 <MenuBarItemTooltip id="title-field">
                     <input
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
index 9ece5078663f0e367dd4ef3bb4af59c001c763cc..46794639d82f5a61eb7f6e084036519c0a913e2e 100644
--- a/src/components/modal/modal.css
+++ b/src/components/modal/modal.css
@@ -24,6 +24,26 @@
     overflow: hidden;
 }
 
+.modal-content.full-screen {
+    position: absolute;
+
+    display: flex;
+    height: 100%;
+    width: 100%;
+
+    overflow-y: auto;
+    -webkit-overflow-scrolling: 'touch';
+    user-select: none;
+
+    background-color: $ui-secondary;
+
+    /* Default modal resets */
+    margin: 0;
+    border: none;
+    border-radius: 0;
+}
+
+
 /*
     Modal header has 3 items:
     |filter     title       x|
@@ -71,9 +91,25 @@ $sides: 20rem;
     user-select: none;
     letter-spacing: 0.4px;
     cursor: default;
+    margin: 0 -$sides 0 0;
+}
+
+.full-screen .header-item-title {
+    margin: 0 0 0 -$sides;
 }
 
 .header-item-close {
     flex-basis: $sides;
     justify-content: flex-end;
+    z-index: 1;
+}
+
+.full-screen .header-item-close {
+    order: -1;
+    justify-content: flex-start;
+}
+
+.back-button {
+    font-weight: normal;
+    padding-left: 0;
 }
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
index 3573e684cf6d32722dcc851709d921095d2f92bc..0683b9bba778394ed82b91f8c2dceeb321aa931f 100644
--- a/src/components/modal/modal.jsx
+++ b/src/components/modal/modal.jsx
@@ -4,70 +4,71 @@ import React from 'react';
 import ReactModal from 'react-modal';
 
 import Box from '../box/box.jsx';
+import Button from '../button/button.jsx';
 import CloseButton from '../close-button/close-button.jsx';
-import Filter from '../filter/filter.jsx';
+
+import backIcon from '../../lib/assets/icon--back.svg';
 
 import styles from './modal.css';
 
-class ModalComponent extends React.Component {
-    render () {
-        return (
-            <ReactModal
-                isOpen
-                className={classNames(styles.modalContent, this.props.className)}
-                contentLabel={this.props.contentLabel}
-                overlayClassName={styles.modalOverlay}
-                ref={m => (this.modal = m)}
-                onRequestClose={this.props.onRequestClose}
-            >
-                <Box
-                    direction="column"
-                    grow={1}
+const ModalComponent = props => (
+    <ReactModal
+        isOpen
+        className={classNames(styles.modalContent, props.className, {
+            [styles.fullScreen]: props.fullScreen
+        })}
+        contentLabel={props.contentLabel}
+        overlayClassName={styles.modalOverlay}
+        onRequestClose={props.onRequestClose}
+    >
+        <Box
+            direction="column"
+            grow={1}
+        >
+            <div className={styles.header}>
+                <div
+                    className={classNames(
+                        styles.headerItem,
+                        styles.headerItemTitle
+                    )}
                 >
-                    <div className={styles.header}>
-                        <div className={classNames(styles.headerItem, styles.headerItemFilter)}>
-                            {this.props.onFilterChange ? (
-                                <Filter
-                                    filterQuery={this.props.filterQuery}
-                                    onChange={this.props.onFilterChange}
-                                    onClear={this.props.onFilterClear}
-                                />
-                            ) : null}
-                        </div>
-                        <div
-                            className={classNames(
-                                styles.headerItem,
-                                styles.headerItemTitle
-                            )}
-                        >
-                            {this.props.contentLabel}
-                        </div>
-                        <div
-                            className={classNames(
-                                styles.headerItem,
-                                styles.headerItemClose
-                            )}
+                    {props.contentLabel}
+                </div>
+                <div
+                    className={classNames(
+                        styles.headerItem,
+                        styles.headerItemClose
+                    )}
+                >
+                    {props.fullScreen ? (
+                        <Button
+                            className={styles.backButton}
+                            iconSrc={backIcon}
+                            onClick={props.onRequestClose}
                         >
-                            <CloseButton
-                                size={CloseButton.SIZE_LARGE}
-                                onClick={this.props.onRequestClose}
-                            />
-                        </div>
-                    </div>
-                    {this.props.children}
-                </Box>
-            </ReactModal>
-        );
-    }
-}
+                            Back
+                        </Button>
+                    ) : (
+                        <CloseButton
+                            size={CloseButton.SIZE_LARGE}
+                            onClick={props.onRequestClose}
+                        />
+                    )}
+                </div>
+            </div>
+            {props.children}
+        </Box>
+    </ReactModal>
+);
 
 ModalComponent.propTypes = {
     children: PropTypes.node,
     className: PropTypes.string,
-    contentLabel: PropTypes.string.isRequired,
-    filterQuery: PropTypes.string,
-    onFilterChange: PropTypes.func,
-    onFilterClear: PropTypes.func,
+    contentLabel: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.object
+    ]).isRequired,
+    fullScreen: PropTypes.bool,
     onRequestClose: PropTypes.func
 };
 
diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx
index 8af363c09312afda80c554e75ecd0627b923c362..66fbc1fe9e20bf3afdc3944daa45091e94fe6d1b 100644
--- a/src/components/stage-selector/stage-selector.jsx
+++ b/src/components/stage-selector/stage-selector.jsx
@@ -32,16 +32,19 @@ const messages = defineMessages({
     addBackdropFromFile: {
         id: 'gui.stageSelector.addBackdropFromFile',
         description: 'Button to add a stage in the target pane from file',
-        defaultMessage: 'Coming Soon'
+        defaultMessage: 'Upload Backdrop'
     }
 });
 
 const StageSelector = props => {
     const {
         backdropCount,
+        fileInputRef,
         intl,
         selected,
         url,
+        onBackdropFileUploadClick,
+        onBackdropFileUpload,
         onClick,
         onNewBackdropClick,
         onSurpriseBackdropClick,
@@ -81,7 +84,11 @@ const StageSelector = props => {
                 moreButtons={[
                     {
                         title: intl.formatMessage(messages.addBackdropFromFile),
-                        img: fileUploadIcon
+                        img: fileUploadIcon,
+                        onClick: onBackdropFileUploadClick,
+                        fileAccept: '.svg, .png, .jpg, .jpeg', // Bitmap coming soon
+                        fileChange: onBackdropFileUpload,
+                        fileInput: fileInputRef
                     }, {
                         title: intl.formatMessage(messages.addBackdropFromSurprise),
                         img: surpriseIcon,
@@ -102,7 +109,10 @@ const StageSelector = props => {
 
 StageSelector.propTypes = {
     backdropCount: PropTypes.number.isRequired,
+    fileInputRef: PropTypes.func,
     intl: intlShape.isRequired,
+    onBackdropFileUpload: PropTypes.func,
+    onBackdropFileUploadClick: PropTypes.func,
     onClick: PropTypes.func,
     onEmptyBackdropClick: PropTypes.func,
     onNewBackdropClick: PropTypes.func,
diff --git a/src/components/tag-button/tag-button.css b/src/components/tag-button/tag-button.css
new file mode 100644
index 0000000000000000000000000000000000000000..b770045138c95a7751d13c4947d28ea32cb3f38f
--- /dev/null
+++ b/src/components/tag-button/tag-button.css
@@ -0,0 +1,19 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+
+.tag-button {
+    padding: .625rem 1rem;
+    background: $motion-primary;
+    border-radius: 1.375rem;
+    color: $ui-white;
+    height: $library-filter-bar-height;
+}
+
+.tag-button-icon {
+    max-width: 1rem;
+    max-height: 1rem;
+}
+
+.active {
+    background: $data-primary;
+}
diff --git a/src/components/tag-button/tag-button.jsx b/src/components/tag-button/tag-button.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e1318f90d993ea74d3ac59faf5719a522fe7ef6
--- /dev/null
+++ b/src/components/tag-button/tag-button.jsx
@@ -0,0 +1,46 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Button from '../button/button.jsx';
+
+import styles from './tag-button.css';
+
+const TagButtonComponent = ({
+    active,
+    iconClassName,
+    className,
+    title,
+    ...props
+}) => (
+    <Button
+        className={classNames(
+            styles.tagButton,
+            className, {
+                [styles.active]: active
+            }
+        )}
+        iconClassName={classNames(
+            styles.tagButtonIcon,
+            iconClassName
+        )}
+        {...props}
+    >
+        {title}
+    </Button>
+);
+
+TagButtonComponent.propTypes = {
+    ...Button.propTypes,
+    active: PropTypes.bool,
+    title: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.object // FormattedMessage
+    ]).isRequired
+};
+
+TagButtonComponent.defaultProps = {
+    active: false
+};
+
+export default TagButtonComponent;
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 7cc7a1ed25963029ea60a6fd3decfb69c52722df..4be489f6948e718c7fe8c2cecb90c44db388726a 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -89,9 +89,11 @@ const spriteShape = PropTypes.shape({
     costume: PropTypes.shape({
         url: PropTypes.string,
         name: PropTypes.string.isRequired,
-        bitmapResolution: PropTypes.number.isRequired,
-        rotationCenterX: PropTypes.number.isRequired,
-        rotationCenterY: PropTypes.number.isRequired
+        // The following are optional because costumes uploaded from disk
+        // will not have these properties available
+        bitmapResolution: PropTypes.number,
+        rotationCenterX: PropTypes.number,
+        rotationCenterY: PropTypes.number
     }),
     direction: PropTypes.number,
     id: PropTypes.string,
diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx
index 83dacab9df8b1d1dddd08a90938e634ba87bf5c6..98604bf18918ee3c30fd687d53f2820a5a7e8a15 100644
--- a/src/containers/backdrop-library.jsx
+++ b/src/containers/backdrop-library.jsx
@@ -1,12 +1,22 @@
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
 import VM from 'scratch-vm';
 
 import analytics from '../lib/analytics';
 import backdropLibraryContent from '../lib/libraries/backdrops.json';
+import backdropTags from '../lib/libraries/backdrop-tags';
 import LibraryComponent from '../components/library/library.jsx';
 
+const messages = defineMessages({
+    libraryTitle: {
+        defaultMessage: 'Choose a Backdrop',
+        description: 'Heading for the backdrop library',
+        id: 'gui.costumeLibrary.chooseABackdrop'
+    }
+});
+
 
 class BackdropLibrary extends React.Component {
     constructor (props) {
@@ -34,7 +44,9 @@ class BackdropLibrary extends React.Component {
         return (
             <LibraryComponent
                 data={backdropLibraryContent}
-                title="Choose a Backdrop"
+                id="backdropLibrary"
+                tags={backdropTags}
+                title={this.props.intl.formatMessage(messages.libraryTitle)}
                 onItemSelected={this.handleItemSelect}
                 onRequestClose={this.props.onRequestClose}
             />
@@ -43,8 +55,9 @@ class BackdropLibrary extends React.Component {
 }
 
 BackdropLibrary.propTypes = {
+    intl: intlShape.isRequired,
     onRequestClose: PropTypes.func,
     vm: PropTypes.instanceOf(VM).isRequired
 };
 
-export default BackdropLibrary;
+export default injectIntl(BackdropLibrary);
diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx
index 3c1e639e26aee884052381e4d0a430c3c1748b1f..fdef46f07fe03192469f3e79583193f77de9d038 100644
--- a/src/containers/costume-library.jsx
+++ b/src/containers/costume-library.jsx
@@ -1,12 +1,22 @@
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
 import VM from 'scratch-vm';
 
 import analytics from '../lib/analytics';
 import costumeLibraryContent from '../lib/libraries/costumes.json';
+import spriteTags from '../lib/libraries/sprite-tags';
 import LibraryComponent from '../components/library/library.jsx';
 
+const messages = defineMessages({
+    libraryTitle: {
+        defaultMessage: 'Choose a Costume',
+        description: 'Heading for the costume library',
+        id: 'gui.costumeLibrary.chooseACostume'
+    }
+});
+
 
 class CostumeLibrary extends React.PureComponent {
     constructor (props) {
@@ -34,7 +44,9 @@ class CostumeLibrary extends React.PureComponent {
         return (
             <LibraryComponent
                 data={costumeLibraryContent}
-                title="Choose a Costume"
+                id="costumeLibrary"
+                tags={spriteTags}
+                title={this.props.intl.formatMessage(messages.libraryTitle)}
                 onItemSelected={this.handleItemSelected}
                 onRequestClose={this.props.onRequestClose}
             />
@@ -43,8 +55,9 @@ class CostumeLibrary extends React.PureComponent {
 }
 
 CostumeLibrary.propTypes = {
+    intl: intlShape.isRequired,
     onRequestClose: PropTypes.func,
     vm: PropTypes.instanceOf(VM).isRequired
 };
 
-export default CostumeLibrary;
+export default injectIntl(CostumeLibrary);
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 43a98bd430321e6d894279509ba39758a00de867..6042ceaaac8003eb03e4594faa2a91eb5b563b77 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -9,6 +9,7 @@ import PaintEditorWrapper from './paint-editor-wrapper.jsx';
 import CostumeLibrary from './costume-library.jsx';
 import BackdropLibrary from './backdrop-library.jsx';
 import {connect} from 'react-redux';
+import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
 
 import {
     closeCostumeLibrary,
@@ -48,9 +49,14 @@ const messages = defineMessages({
         description: 'Button to add a surprise costume in the editor tab',
         id: 'gui.costumeTab.addSurpriseCostume'
     },
+    addFileBackdropMsg: {
+        defaultMessage: 'Upload Backdrop',
+        description: 'Button to add a backdrop by uploading a file in the editor tab',
+        id: 'gui.costumeTab.addFileBackdrop'
+    },
     addFileCostumeMsg: {
-        defaultMessage: 'Coming Soon',
-        description: 'Button to add a file upload costume in the editor tab',
+        defaultMessage: 'Upload Costume',
+        description: 'Button to add a costume by uploading a file in the editor tab',
         id: 'gui.costumeTab.addFileCostume'
     },
     addCameraCostumeMsg: {
@@ -67,9 +73,13 @@ class CostumeTab extends React.Component {
             'handleSelectCostume',
             'handleDeleteCostume',
             'handleDuplicateCostume',
+            'handleNewCostume',
             'handleNewBlankCostume',
             'handleSurpriseCostume',
-            'handleSurpriseBackdrop'
+            'handleSurpriseBackdrop',
+            'handleFileUploadClick',
+            'handleCostumeUpload',
+            'setFileInput'
         ]);
         const {
             editingTarget,
@@ -121,6 +131,9 @@ class CostumeTab extends React.Component {
     handleDuplicateCostume (costumeIndex) {
         this.props.vm.duplicateCostume(costumeIndex);
     }
+    handleNewCostume (costume) {
+        this.props.vm.addCostume(costume.md5, costume);
+    }
     handleNewBlankCostume () {
         const emptyItem = costumeLibraryContent.find(item => (
             item.name === 'Empty'
@@ -128,35 +141,50 @@ class CostumeTab extends React.Component {
         const name = this.props.vm.editingTarget.isStage ? `backdrop1` : `costume1`;
         const vmCostume = {
             name: name,
+            md5: emptyItem.md5,
             rotationCenterX: emptyItem.info[0],
             rotationCenterY: emptyItem.info[1],
             bitmapResolution: emptyItem.info.length > 2 ? emptyItem.info[2] : 1,
             skinId: null
         };
 
-        this.props.vm.addCostume(emptyItem.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
     }
     handleSurpriseCostume () {
         const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
         const vmCostume = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0],
             rotationCenterY: item.info[1],
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.props.vm.addCostume(item.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
     }
     handleSurpriseBackdrop () {
         const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
         const vmCostume = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0] && item.info[0] / 2,
             rotationCenterY: item.info[1] && item.info[1] / 2,
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.props.vm.addCostume(item.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
+    }
+    handleCostumeUpload (e) {
+        const storage = this.props.vm.runtime.storage;
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume);
+        });
+    }
+    handleFileUploadClick () {
+        this.fileInput.click();
+    }
+    setFileInput (input) {
+        this.fileInput = input;
     }
     formatCostumeDetails (size) {
         // Round up width and height for scratch-flash compatibility
@@ -185,6 +213,7 @@ class CostumeTab extends React.Component {
         }
 
         const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg;
+        const addFileMessage = target.isStage ? messages.addFileBackdropMsg : messages.addFileCostumeMsg;
         const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume;
         const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick;
         const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon;
@@ -208,8 +237,12 @@ class CostumeTab extends React.Component {
                         img: cameraIcon
                     },
                     {
-                        title: intl.formatMessage(messages.addFileCostumeMsg),
-                        img: fileUploadIcon
+                        title: intl.formatMessage(addFileMessage),
+                        img: fileUploadIcon,
+                        onClick: this.handleFileUploadClick,
+                        fileAccept: '.svg, .png, .jpg, .jpeg', // coming soon
+                        fileChange: this.handleCostumeUpload,
+                        fileInput: this.setFileInput
                     },
                     {
                         title: intl.formatMessage(messages.addSurpriseCostumeMsg),
diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx
index dfc00b5bc8bf1af27b8faaec54254571b8022d15..7605fd4b6501388c40a111667b4f80b5dada8d65 100644
--- a/src/containers/custom-procedures.jsx
+++ b/src/containers/custom-procedures.jsx
@@ -75,7 +75,7 @@ class CustomProcedures extends React.Component {
         this.props.onRequestClose();
     }
     handleOk () {
-        const newMutation = this.mutationRoot ? this.mutationRoot.mutationToDom() : null;
+        const newMutation = this.mutationRoot ? this.mutationRoot.mutationToDom(true) : null;
         this.props.onRequestClose(newMutation);
     }
     handleAddLabel () {
diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx
index 5d602cfbd03529da5ec710919f4c98b28e125963..d30daf98a908210e2db25d9a5a8984619c3ff9dd 100644
--- a/src/containers/extension-library.jsx
+++ b/src/containers/extension-library.jsx
@@ -45,6 +45,8 @@ class ExtensionLibrary extends React.PureComponent {
         return (
             <LibraryComponent
                 data={extensionLibraryThumbnailData}
+                filterable={false}
+                id="extensionLibrary"
                 title="Choose an Extension"
                 visible={this.props.visible}
                 onItemSelected={this.handleItemSelect}
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 22dccba2cb53664e76615766e537ebe6e8eb2a96..73e5f337518da9d50a09d69c11aa3e7603fee475 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -12,6 +12,8 @@ import {
     SOUNDS_TAB_INDEX
 } from '../reducers/editor-tab';
 
+import AppStateHOC from '../lib/app-state-hoc.jsx';
+import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
 import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
 
 import GUIComponent from '../components/gui/gui.jsx';
@@ -87,7 +89,7 @@ GUI.propTypes = {
     importInfoVisible: PropTypes.bool,
     loadingStateVisible: PropTypes.bool,
     previewInfoVisible: PropTypes.bool,
-    projectData: PropTypes.string,
+    projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
     vm: PropTypes.instanceOf(VM)
 };
 
@@ -115,4 +117,4 @@ const ConnectedGUI = connect(
     mapDispatchToProps,
 )(GUI);
 
-export default vmListenerHOC(ConnectedGUI);
+export default ProjectLoaderHOC(AppStateHOC(vmListenerHOC(ConnectedGUI)));
diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx
index ae0d2405cffb888abd2d8500a93ea382f3b53c5b..316b1577a367a4314ee7a610f72b9bee2516874d 100644
--- a/src/containers/language-selector.jsx
+++ b/src/containers/language-selector.jsx
@@ -1,5 +1,4 @@
 import {connect} from 'react-redux';
-import {updateIntl} from '../reducers/intl.js';
 
 import LanguageSelectorComponent from '../components/language-selector/language-selector.jsx';
 
@@ -7,10 +6,9 @@ const mapStateToProps = state => ({
     currentLocale: state.intl.locale
 });
 
-const mapDispatchToProps = dispatch => ({
+const mapDispatchToProps = () => ({
     onChange: e => {
         e.preventDefault();
-        dispatch(updateIntl(e.target.value));
     }
 });
 
diff --git a/src/containers/modal.jsx b/src/containers/modal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..67b14c9ff4494b1e5f354f7befb0b6b0faa9946b
--- /dev/null
+++ b/src/containers/modal.jsx
@@ -0,0 +1,54 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import ModalComponent from '../components/modal/modal.jsx';
+
+class Modal extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'addEventListeners',
+            'removeEventListeners',
+            'handlePopState',
+            'pushHistory'
+        ]);
+        this.addEventListeners();
+    }
+    componentDidMount () {
+        // Add a history event only if it's not currently for our modal. This
+        // avoids polluting the history with many entries. We only need one.
+        this.pushHistory(this.id, history.state === null);
+    }
+    componentWillUnmount () {
+        this.removeEventListeners();
+    }
+    addEventListeners () {
+        window.addEventListener('popstate', this.handlePopState);
+    }
+    removeEventListeners () {
+        window.removeEventListener('popstate', this.handlePopState);
+    }
+    handlePopState () {
+        // Whenever someone navigates, we want to be closed
+        this.props.onRequestClose();
+    }
+    get id () {
+        return `modal-${this.props.id}`;
+    }
+    pushHistory (state, push) {
+        if (push) return history.pushState(state, this.id);
+        history.replaceState(state, this.id);
+    }
+    render () {
+        return <ModalComponent {...this.props} />;
+    }
+}
+
+Modal.propTypes = {
+    id: PropTypes.string.isRequired,
+    onRequestClose: PropTypes.func,
+    onRequestOpen: PropTypes.func
+};
+
+export default Modal;
diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx
index cc333dc78d9e295f6fd59d44dc1abc72a0d4cb43..3d68ea6f32ad19134508c847321147c4f51daf93 100644
--- a/src/containers/sound-library.jsx
+++ b/src/containers/sound-library.jsx
@@ -1,6 +1,7 @@
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
 import VM from 'scratch-vm';
 import AudioEngine from 'scratch-audio';
 
@@ -10,6 +11,15 @@ import LibraryComponent from '../components/library/library.jsx';
 import soundIcon from '../components/asset-panel/icon--sound.svg';
 
 import soundLibraryContent from '../lib/libraries/sounds.json';
+import soundTags from '../lib/libraries/sound-tags';
+
+const messages = defineMessages({
+    libraryTitle: {
+        defaultMessage: 'Choose a Sound',
+        description: 'Heading for the sound library',
+        id: 'gui.soundLibrary.chooseASound'
+    }
+});
 
 class SoundLibrary extends React.PureComponent {
     constructor (props) {
@@ -83,7 +93,9 @@ class SoundLibrary extends React.PureComponent {
         return (
             <LibraryComponent
                 data={soundLibraryThumbnailData}
-                title="Choose a Sound"
+                id="soundLibrary"
+                tags={soundTags}
+                title={this.props.intl.formatMessage(messages.libraryTitle)}
                 onItemMouseEnter={this.handleItemMouseEnter}
                 onItemMouseLeave={this.handleItemMouseLeave}
                 onItemSelected={this.handleItemSelected}
@@ -94,9 +106,10 @@ class SoundLibrary extends React.PureComponent {
 }
 
 SoundLibrary.propTypes = {
+    intl: intlShape.isRequired,
     onNewSound: PropTypes.func.isRequired,
     onRequestClose: PropTypes.func,
     vm: PropTypes.instanceOf(VM).isRequired
 };
 
-export default SoundLibrary;
+export default injectIntl(SoundLibrary);
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 94e72125d15f62333c10de5fee7e704f8a425626..ab4c9062bdebc5fa1b5055919a0df50bc1c3f3a3 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -16,6 +16,7 @@ import SoundEditor from './sound-editor.jsx';
 import SoundLibrary from './sound-library.jsx';
 
 import soundLibraryContent from '../lib/libraries/sounds.json';
+import {handleFileUpload, soundUpload} from '../lib/file-uploader.js';
 
 import {connect} from 'react-redux';
 
@@ -106,41 +107,13 @@ class SoundTab extends React.Component {
     }
 
     handleSoundUpload (e) {
-        const thisFileInput = e.target;
-        let thisFile = null;
-        const reader = new FileReader();
-        reader.onload = () => {
-            // Reset the file input value now that we have everything we need
-            // so that the user can upload the same sound multiple times if
-            // they choose
-            thisFileInput.value = null;
-            // Cache the sound in storage
-            const soundBuffer = reader.result;
-            const storage = this.props.vm.runtime.storage;
+        const storage = this.props.vm.runtime.storage;
+        const handleSound = newSound => this.props.vm.addSound(newSound)
+            .then(() => this.handleNewSound());
 
-            const fileType = thisFile.type; // what file type does the browser think this is
-            const soundFormat = fileType === 'audio/mp3' ? storage.DataFormat.MP3 : storage.DataFormat.WAV;
-            const md5 = storage.builtinHelper.cache(
-                storage.AssetType.Sound,
-                soundFormat,
-                new Uint8Array(soundBuffer),
-            );
-            // Add the sound to vm
-            const newSound = {
-                format: '',
-                name: 'sound1',
-                dataFormat: soundFormat,
-                md5: `${md5}.${soundFormat}`
-            };
-
-            this.props.vm.addSound(newSound).then(() => {
-                this.handleNewSound();
-            });
-        };
-        if (thisFileInput.files) {
-            thisFile = thisFileInput.files[0];
-            reader.readAsArrayBuffer(thisFile);
-        }
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            soundUpload(buffer, fileType, fileName, storage, handleSound);
+        });
     }
 
     setFileInput (input) {
diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx
index ebfd401af7a7a3c47e3c469bc0be0ceae9a927f9..072e1714a81ca1683acb86e9d280bef47c322156 100644
--- a/src/containers/sprite-library.jsx
+++ b/src/containers/sprite-library.jsx
@@ -1,13 +1,23 @@
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {injectIntl, intlShape, defineMessages} from 'react-intl';
 import VM from 'scratch-vm';
 
 import analytics from '../lib/analytics';
 import spriteLibraryContent from '../lib/libraries/sprites.json';
+import spriteTags from '../lib/libraries/sprite-tags';
 
 import LibraryComponent from '../components/library/library.jsx';
 
+const messages = defineMessages({
+    libraryTitle: {
+        defaultMessage: 'Choose a Sprite',
+        description: 'Heading for the sprite library',
+        id: 'gui.spriteLibrary.chooseASprite'
+    }
+});
+
 class SpriteLibrary extends React.PureComponent {
     constructor (props) {
         super(props);
@@ -71,7 +81,9 @@ class SpriteLibrary extends React.PureComponent {
         return (
             <LibraryComponent
                 data={this.state.sprites}
-                title="Choose a Sprite"
+                id="spriteLibrary"
+                tags={spriteTags}
+                title={this.props.intl.formatMessage(messages.libraryTitle)}
                 onItemMouseEnter={this.handleMouseEnter}
                 onItemMouseLeave={this.handleMouseLeave}
                 onItemSelected={this.handleItemSelect}
@@ -82,8 +94,9 @@ class SpriteLibrary extends React.PureComponent {
 }
 
 SpriteLibrary.propTypes = {
+    intl: intlShape.isRequired,
     onRequestClose: PropTypes.func,
     vm: PropTypes.instanceOf(VM).isRequired
 };
 
-export default SpriteLibrary;
+export default injectIntl(SpriteLibrary);
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index bf749c12a08fa7d997e484aa19b98d252e2a9d30..f3497a54cd9f2e187ffc5a5586b20f7492027f0c 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -10,47 +10,64 @@ import StageSelectorComponent from '../components/stage-selector/stage-selector.
 
 import backdropLibraryContent from '../lib/libraries/backdrops.json';
 import costumeLibraryContent from '../lib/libraries/costumes.json';
+import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
 
 class StageSelector extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
             'handleClick',
+            'handleNewBackdrop',
             'handleSurpriseBackdrop',
             'handleEmptyBackdrop',
-            'addBackdropFromLibraryItem'
+            'addBackdropFromLibraryItem',
+            'handleFileUploadClick',
+            'handleBackdropUpload',
+            'setFileInput'
         ]);
     }
     addBackdropFromLibraryItem (item) {
         const vmBackdrop = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0] && item.info[0] / 2,
             rotationCenterY: item.info[1] && item.info[1] / 2,
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        return this.props.vm.addBackdrop(item.md5, vmBackdrop);
+        this.handleNewBackdrop(vmBackdrop);
     }
-    handleClick (e) {
-        e.preventDefault();
+    handleClick () {
         this.props.onSelect(this.props.id);
     }
+    handleNewBackdrop (backdrop) {
+        this.props.vm.addBackdrop(backdrop.md5, backdrop).then(() =>
+            this.props.onActivateTab(COSTUMES_TAB_INDEX));
+    }
     handleSurpriseBackdrop () {
         // @todo should this not add a backdrop you already have?
         const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
-        this.addBackdropFromLibraryItem(item).then(() => {
-            this.props.onActivateTab(COSTUMES_TAB_INDEX);
-        });
+        this.addBackdropFromLibraryItem(item);
     }
     handleEmptyBackdrop () {
         // @todo this is brittle, will need to be refactored for localized libraries
         const emptyItem = costumeLibraryContent.find(item => item.name === 'Empty');
         if (emptyItem) {
-            this.addBackdropFromLibraryItem(emptyItem).then(() => {
-                this.props.onActivateTab(COSTUMES_TAB_INDEX);
-            });
+            this.addBackdropFromLibraryItem(emptyItem);
         }
     }
+    handleBackdropUpload (e) {
+        const storage = this.props.vm.runtime.storage;
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            costumeUpload(buffer, fileType, fileName, storage, this.handleNewBackdrop);
+        });
+    }
+    handleFileUploadClick () {
+        this.fileInput.click();
+    }
+    setFileInput (input) {
+        this.fileInput = input;
+    }
     render () {
         const {
             /* eslint-disable no-unused-vars */
@@ -63,9 +80,13 @@ class StageSelector extends React.Component {
         } = this.props;
         return (
             <StageSelectorComponent
+                fileInputRef={this.setFileInput}
+                onBackdropFileUpload={this.handleBackdropUpload}
+                onBackdropFileUploadClick={this.handleFileUploadClick}
                 onClick={this.handleClick}
                 onEmptyBackdropClick={this.handleEmptyBackdrop}
                 onSurpriseBackdropClick={this.handleSurpriseBackdrop}
+
                 {...componentProps}
             />
         );
diff --git a/src/containers/tag-button.jsx b/src/containers/tag-button.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5ad61f4080abbaced07fe2c6f7ab45bcca7f086
--- /dev/null
+++ b/src/containers/tag-button.jsx
@@ -0,0 +1,36 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import TagButtonComponent from '../components/tag-button/tag-button.jsx';
+
+class TagButton extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleClick'
+        ]);
+    }
+    handleClick () {
+        this.props.onClick(this.props.title);
+    }
+    render () {
+        return (
+            <TagButtonComponent
+                {...this.props}
+                onClick={this.handleClick}
+            />
+        );
+    }
+}
+
+TagButton.propTypes = {
+    ...TagButtonComponent.propTypes,
+    onClick: PropTypes.func,
+    title: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.object
+    ]).isRequired
+};
+
+export default TagButton;
diff --git a/src/css/units.css b/src/css/units.css
index 3e7f73f36d327fa55f464c4b49577661f58b4799..ef34280983f2c54b1fa67c9ecf50e00b141396dc 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -6,7 +6,8 @@ $menu-bar-height: 3rem;
 $sprite-info-height: 6rem;
 $stage-menu-height: 2.75rem;
 
-$library-header-height: 4.375rem;
+$library-header-height: 3.125rem;
+$library-filter-bar-height: 2.5rem;
 
 $form-radius: calc($space / 2);
 
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index 2894cda13e372e2936868b6cd913c9f040f9bb45..e593fa40b304624e01b472695f00ad6546046987 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -3,7 +3,9 @@ import {Provider} from 'react-redux';
 import {createStore, applyMiddleware, compose} from 'redux';
 import throttle from 'redux-throttle';
 
-import {intlInitialState, IntlProvider} from '../reducers/intl.js';
+import {intlShape} from 'react-intl';
+import {IntlProvider, updateIntl} from 'react-intl-redux';
+import {intlInitialState} from '../reducers/intl.js';
 import reducer from '../reducers/gui';
 
 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@@ -12,25 +14,51 @@ const enhancer = composeEnhancers(
         throttle(300, {leading: true, trailing: true})
     )
 );
-const store = createStore(reducer, intlInitialState, enhancer);
-
-import ErrorBoundary from '../containers/error-boundary.jsx';
 
 /*
- * Higher Order Component to provide redux state
+ * Higher Order Component to provide redux state. If an `intl` prop is provided
+ * it will override the internal `intl` 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>
-                <ErrorBoundary>
-                    <WrappedComponent {...props} />
-                </ErrorBoundary>
-            </IntlProvider>
-        </Provider>
-    );
+    class AppStateWrapper extends React.Component {
+        constructor (props) {
+            super(props);
+            let intl = {};
+            if (props.intl) {
+                intl = {
+                    intl: {
+                        defaultLocale: 'en',
+                        locale: props.intl.locale,
+                        messages: props.intl.messages
+                    }
+                };
+            } else {
+                intl = intlInitialState;
+            }
+
+            this.store = createStore(reducer, intl, enhancer);
+        }
+        componentDidUpdate (prevProps) {
+            if (prevProps.intl !== this.props.intl) updateIntl(this.props.intl);
+        }
+        render () {
+            return (
+                <Provider store={this.store}>
+                    <IntlProvider>
+                        <WrappedComponent {...this.props} />
+                    </IntlProvider>
+                </Provider>
+            );
+
+        }
+
+
+    }
+    AppStateWrapper.propTypes = {
+        intl: intlShape
+    };
     return AppStateWrapper;
 };
 
diff --git a/src/components/close-button/icon--back.svg b/src/lib/assets/icon--back.svg
similarity index 100%
rename from src/components/close-button/icon--back.svg
rename to src/lib/assets/icon--back.svg
diff --git a/src/lib/assets/placeholder.svg b/src/lib/assets/placeholder.svg
new file mode 100644
index 0000000000000000000000000000000000000000..67be6428a849d2dccfc43fa657213fc2ac8afbc9
Binary files /dev/null and b/src/lib/assets/placeholder.svg differ
diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js
new file mode 100644
index 0000000000000000000000000000000000000000..cea0aaeb11abc1258672bd0220abd0522ea76b33
--- /dev/null
+++ b/src/lib/file-uploader.js
@@ -0,0 +1,173 @@
+import {importBitmap} from 'scratch-svg-renderer';
+import log from './log.js';
+
+/**
+ * Extract the file name given a string of the form fileName + ext
+ * @param {string} nameExt File name + extension (e.g. 'my_image.png')
+ * @return {string} The name without the extension, or the full name if
+ * there was no '.' in the string (e.g. 'my_image')
+ */
+const extractFileName = function (nameExt) {
+    // There could be multiple dots, but get the stuff before the first .
+    const nameParts = nameExt.split('.', 1); // we only care about the first .
+    return nameParts[0];
+};
+
+/**
+ * Handle a file upload given the input element that contains the file,
+ * and a function to handle loading the file.
+ * @param {Input} fileInput The <input/> element that contains the file being loaded
+ * @param {Function} onload The function that handles loading the file
+ */
+const handleFileUpload = function (fileInput, onload) {
+    let thisFile = null;
+    const reader = new FileReader();
+    reader.onload = () => {
+        // Reset the file input value now that we have everything we need
+        // so that the user can upload the same sound multiple times if
+        // they choose
+        fileInput.value = null;
+        const fileType = thisFile.type;
+        const fileName = extractFileName(thisFile.name);
+
+        onload(reader.result, fileType, fileName);
+    };
+    if (fileInput.files) {
+        thisFile = fileInput.files[0];
+        reader.readAsArrayBuffer(thisFile);
+    }
+};
+
+/**
+ * @typedef VMAsset
+ * @property {string} name The user-readable name of this asset - This will
+ * automatically get translated to a fresh name if this one already exists in the
+ * scope of this vm asset (e.g. if a sound already exists with the same name for
+ * the same target)
+ * @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
+ */
+
+/**
+ * Cache an asset (costume, sound) in storage and return an object representation
+ * of the asset to track in the VM.
+ * @param {ScratchStorage} storage The storage to cache the asset in
+ * @param {string} fileName The name of the asset
+ * @param {AssetType} assetType A ScratchStorage AssetType indicating what kind of
+ * asset this is.
+ * @param {string} dataFormat The format of this data (typically the file extension)
+ * @param {UInt8Array} data The asset data buffer
+ * @return {VMAsset} An object representing this asset and relevant information
+ * which can be used to look up the data in storage
+ */
+const cacheAsset = function (storage, fileName, assetType, dataFormat, data) {
+    const md5 = storage.builtinHelper.cache(
+        assetType,
+        dataFormat,
+        data
+    );
+
+    return {
+        name: fileName,
+        dataFormat: dataFormat,
+        md5: `${md5}.${dataFormat}`
+    };
+};
+
+/**
+ * Handles loading a costume or a backdrop using the provided, context-relevant information.
+ * @param {ArrayBuffer} fileData The costume data to load
+ * @param {string} fileType The MIME type of this file
+ * @param {string} costumeName The user-readable name to use for the costume.
+ * @param {ScratchStorage} storage The ScratchStorage instance to cache the costume data
+ * @param {Function} handleCostume The function to execute on the costume object returned after
+ * caching this costume in storage - This function should be responsible for
+ * adding the costume to the VM and handling other UI flow that should come after adding the costume
+ */
+const costumeUpload = function (fileData, fileType, costumeName, storage, handleCostume) {
+    let costumeFormat = null;
+    let assetType = null;
+    switch (fileType) {
+    case 'image/svg+xml': {
+        costumeFormat = storage.DataFormat.SVG;
+        assetType = storage.AssetType.ImageVector;
+        break;
+    }
+    case 'image/jpeg': {
+        costumeFormat = storage.DataFormat.JPG;
+        assetType = storage.AssetType.ImageBitmap;
+        break;
+    }
+    case 'image/png': {
+        costumeFormat = storage.DataFormat.PNG;
+        assetType = storage.AssetType.ImageBitmap;
+        break;
+    }
+    default:
+        return;
+    }
+
+    const addCostumeFromBuffer = function (error, costumeBuffer) {
+        if (error) {
+            log.warn(`An error occurred while trying to extract image data: ${error}`);
+            return;
+        }
+
+        const vmCostume = cacheAsset(storage, costumeName, assetType, costumeFormat, costumeBuffer);
+        handleCostume(vmCostume);
+    };
+
+    if (costumeFormat === storage.DataFormat.SVG) {
+        // Must pass in file data as a Uint8Array,
+        // passing in an array buffer causes the sprite/costume
+        // thumbnails to not display because the data URI for the costume
+        // is invalid
+        addCostumeFromBuffer(null, new Uint8Array(fileData));
+    } else {
+        // otherwise it's a bitmap
+        importBitmap(fileData, addCostumeFromBuffer);
+    }
+};
+
+/**
+ * Handles loading a sound using the provided, context-relevant information.
+ * @param {ArrayBuffer} fileData The sound data to load
+ * @param {string} fileType The MIME type of this file; This function will exit
+ * early if the fileType is unexpected.
+ * @param {string} soundName The user-readable name to use for the sound.
+  * @param {ScratchStorage} storage The ScratchStorage instance to cache the sound data
+ * @param {Function} handleSound The function to execute on the sound object of type VMAsset
+ * This function should be responsible for adding the sound to the VM
+ * as well as handling other UI flow that should come after adding the sound
+ */
+const soundUpload = function (fileData, fileType, soundName, storage, handleSound) {
+    let soundFormat;
+    switch (fileType) {
+    case 'audio/mp3': {
+        soundFormat = storage.DataFormat.MP3;
+        break;
+    }
+    case 'audio/wav': { // TODO support audio/x-wav? Do we see this in the wild?
+        soundFormat = storage.DataFormat.WAV;
+        break;
+    }
+    default:
+        return;
+    }
+
+    const vmSound = cacheAsset(
+        storage,
+        soundName,
+        storage.AssetType.Sound,
+        soundFormat,
+        new Uint8Array(fileData));
+
+    handleSound(vmSound);
+};
+
+export {
+    handleFileUpload,
+    costumeUpload,
+    soundUpload
+};
diff --git a/src/lib/libraries/backdrop-tags.js b/src/lib/libraries/backdrop-tags.js
new file mode 100644
index 0000000000000000000000000000000000000000..7bafa3410aa3afe94692d0e4d495a9f8ecaeb256
--- /dev/null
+++ b/src/lib/libraries/backdrop-tags.js
@@ -0,0 +1,10 @@
+export default [
+    {title: 'Fantasy'},
+    {title: 'Music'},
+    {title: 'Sports'},
+    {title: 'Outdoors'},
+    {title: 'Indoors'},
+    {title: 'Space'},
+    {title: 'Underwater'},
+    {title: 'Patterns'}
+];
diff --git a/src/lib/libraries/extensions/index.js b/src/lib/libraries/extensions/index.js
index 29b3b8012948e62d37e4ef24ea3e8bb5123db149..670df71eef4a847c3b4462f037056506fb30661e 100644
--- a/src/lib/libraries/extensions/index.js
+++ b/src/lib/libraries/extensions/index.js
@@ -54,7 +54,7 @@ export default [
         disabled: true
     },
     {
-        name: 'LEGO Mindstorms EV3',
+        name: 'LEGO MINDSTORMS EV3',
         extensionURL: '',
         iconURL: ev3Image,
         description: 'Build interactive robots and more.',
diff --git a/src/lib/libraries/sound-tags.js b/src/lib/libraries/sound-tags.js
new file mode 100644
index 0000000000000000000000000000000000000000..c35122797b0a01e0eac827db19444fe07d194d0a
--- /dev/null
+++ b/src/lib/libraries/sound-tags.js
@@ -0,0 +1,11 @@
+export default [
+    {title: 'Animal'},
+    {title: 'Effects'},
+    {title: 'Loops'},
+    {title: 'Notes'},
+    {title: 'Percussion'},
+    {title: 'Space'},
+    {title: 'Sports'},
+    {title: 'Voice'},
+    {title: 'Wacky'}
+];
diff --git a/src/lib/libraries/sprite-tags.js b/src/lib/libraries/sprite-tags.js
new file mode 100644
index 0000000000000000000000000000000000000000..f1fbe3d5647910980f1133dd57aa05569ee05334
--- /dev/null
+++ b/src/lib/libraries/sprite-tags.js
@@ -0,0 +1,10 @@
+export default [
+    {title: 'Animals'},
+    {title: 'People'},
+    {title: 'Fantasy'},
+    {title: 'Dance'},
+    {title: 'Music'},
+    {title: 'Sports'},
+    {title: 'Food'},
+    {title: 'Fashion'}
+];
diff --git a/src/lib/libraries/sprites.json b/src/lib/libraries/sprites.json
index f0aa1bab7f75e552068b29ec4d787712deff0001..7a08b0033a1076e03b8b922659bfb8f0e49d0ddb 100644
--- a/src/lib/libraries/sprites.json
+++ b/src/lib/libraries/sprites.json
@@ -3,7 +3,7 @@
         "name": "Abby",
         "md5": "afab2d2141e9811bd89e385e9628cb5f.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["people"],
         "info": [
             0,
             4,
@@ -71,7 +71,7 @@
         "name": "Apple",
         "md5": "831ccd4741a7a56d85f6698a21f4ca69.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["food"],
         "info": [
             0,
             1,
@@ -183,7 +183,7 @@
         "name": "Ball",
         "md5": "10117ddaefa98d819f2b1df93805622f.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["sports"],
         "info": [
             0,
             5,
@@ -259,7 +259,7 @@
         "name": "Balloon1",
         "md5": "bc96a1fb5fe794377acd44807e421ce2.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             3,
@@ -319,7 +319,7 @@
         "name": "Baseball",
         "md5": "6e13ef53885d2cfca9a54cc5c3f6c20f.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["sports"],
         "info": [
             0,
             1,
@@ -363,7 +363,7 @@
         "name": "Basketball",
         "md5": "b15c425f3eef68e7d095ee91321cb52a.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["sports"],
         "info": [
             0,
             1,
@@ -407,7 +407,7 @@
         "name": "Bat1",
         "md5": "7ad915f8e0884f497a24d5bb61ea8a4a.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals", "fantasy"],
         "info": [
             0,
             2,
@@ -459,7 +459,7 @@
         "name": "Bat2",
         "md5": "647d4bd53163f94a7dabf623ccab7fd3.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals", "fantasy"],
         "info": [
             0,
             2,
@@ -511,7 +511,7 @@
         "name": "Beachball",
         "md5": "87d64cab74c64b31498cc85f07510ee4.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["sports"],
         "info": [
             0,
             1,
@@ -555,7 +555,7 @@
         "name": "Beetle",
         "md5": "e1ce8f153f011fdd52486c91c6ed594d.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals"],
         "info": [
             0,
             1,
@@ -599,7 +599,7 @@
         "name": "Bell",
         "md5": "f35056c772395455d703773657e1da6e.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["music"],
         "info": [
             0,
             1,
@@ -767,7 +767,7 @@
         "name": "Butterfly1",
         "md5": "836d4cc7889f4a1cbcb0303934f31f79.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals"],
         "info": [
             0,
             2,
@@ -819,7 +819,7 @@
         "name": "Butterfly2",
         "md5": "a0216895beade6afc6d42bd5bb43faea.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals", "fantasy"],
         "info": [
             0,
             1,
@@ -1871,7 +1871,7 @@
         "name": "Dog1",
         "md5": "39ddefa0cc58f3b1b06474d63d81ef56.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["animals", "puppy"],
         "info": [
             0,
             2,
@@ -1983,7 +1983,7 @@
         "name": "Donut",
         "md5": "9e7b4d153421dae04a24571d7e079e85.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["food"],
         "info": [
             0,
             1,
@@ -2891,7 +2891,7 @@
         "name": "Ghost1",
         "md5": "c88579c578f2d171de78612f2ff9c9d9.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             1,
@@ -2935,7 +2935,7 @@
         "name": "Ghost2",
         "md5": "607be245da950af1a4e4d79acfda46e3.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             2,
@@ -3191,7 +3191,7 @@
         "name": "Guitar",
         "md5": "cb8c2a5e69da7538e1dd73cb7ff4a666.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["music"],
         "info": [
             0,
             2,
@@ -3299,7 +3299,7 @@
         "name": "Guitar-electric2",
         "md5": "1fc433b89038f9e16092c9f4d7514cca.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["music"],
         "info": [
             0,
             2,
@@ -4483,7 +4483,7 @@
         "name": "Milk",
         "md5": "e6a7964bc4ea38c79a5a31d6ddfb5ba9.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["food"],
         "info": [
             0,
             5,
@@ -5343,7 +5343,7 @@
         "name": "Rainbow",
         "md5": "680a806bd87a28c8b25b5f9b6347f022.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             1,
@@ -5615,7 +5615,7 @@
         "name": "Sam",
         "md5": "7f32d8d78ad64f50c018b7b578de2e18.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["people", "sports"],
         "info": [
             0,
             4,
@@ -6623,7 +6623,7 @@
         "name": "Tera",
         "md5": "b54a4a9087435863ab6f6c908f1cac99.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy", "dance"],
         "info": [
             0,
             4,
@@ -7007,7 +7007,7 @@
         "name": "Wizard",
         "md5": "4df1dd733f6ee4a2d8842478ac2c4661.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             1,
@@ -7051,7 +7051,7 @@
         "name": "Wizard1",
         "md5": "e085c97691d16f0cc8a595ce1137e26c.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             1,
@@ -7095,7 +7095,7 @@
         "name": "Wizard2",
         "md5": "6db4d9e4229dc50a1b1c91c3c8311d40.svg",
         "type": "sprite",
-        "tags": [],
+        "tags": ["fantasy"],
         "info": [
             0,
             1,
@@ -11447,4 +11447,4 @@
             "spriteInfo": {}
         }
     }
-]
\ No newline at end of file
+]
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 10ff86ea6d48b82ade58bb0fce3e6a60b17a5498..9ecb6353ad5dd6ad6c02f1f4774f5eb8a344536d 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -189,7 +189,7 @@ const looks = function (isStage, targetId) {
             </block>
             <block type="looks_nextbackdrop"/>
         ` : `
-            <block type="looks_switchcostumeto">
+            <block id="${targetId}_switchcostumeto" type="looks_switchcostumeto">
                 <value name="COSTUME">
                     <shadow type="looks_costume"/>
                 </value>
@@ -259,15 +259,15 @@ const looks = function (isStage, targetId) {
     `;
 };
 
-const sound = function () {
+const sound = function (isStage, targetId) {
     return `
     <category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
-        <block type="sound_play">
+        <block id="${targetId}_sound_play" type="sound_play">
             <value name="SOUND_MENU">
                 <shadow type="sound_sounds_menu"/>
             </value>
         </block>
-        <block type="sound_playuntildone">
+        <block id="${targetId}_sound_playuntildone" type="sound_playuntildone">
             <value name="SOUND_MENU">
                 <shadow type="sound_sounds_menu"/>
             </value>
@@ -531,7 +531,7 @@ const operators = function () {
             </value>
         </block>
         ${blockSeparator}
-        <block type="operator_lt">
+        <block type="operator_gt">
             <value name="OPERAND1">
                 <shadow type="text">
                     <field name="TEXT"/>
@@ -539,11 +539,11 @@ const operators = function () {
             </value>
             <value name="OPERAND2">
                 <shadow type="text">
-                    <field name="TEXT"/>
+                    <field name="TEXT">100</field>
                 </shadow>
             </value>
         </block>
-        <block type="operator_equals">
+        <block type="operator_lt">
             <value name="OPERAND1">
                 <shadow type="text">
                     <field name="TEXT"/>
@@ -551,11 +551,11 @@ const operators = function () {
             </value>
             <value name="OPERAND2">
                 <shadow type="text">
-                    <field name="TEXT"/>
+                    <field name="TEXT">100</field>
                 </shadow>
             </value>
         </block>
-        <block type="operator_gt">
+        <block type="operator_equals">
             <value name="OPERAND1">
                 <shadow type="text">
                     <field name="TEXT"/>
@@ -563,7 +563,7 @@ const operators = function () {
             </value>
             <value name="OPERAND2">
                 <shadow type="text">
-                    <field name="TEXT"/>
+                    <field name="TEXT">100</field>
                 </shadow>
             </value>
         </block>
@@ -575,12 +575,12 @@ const operators = function () {
         <block type="operator_join">
             <value name="STRING1">
                 <shadow type="text">
-                    <field name="TEXT">hello</field>
+                    <field name="TEXT">apple</field>
                 </shadow>
             </value>
             <value name="STRING2">
                 <shadow type="text">
-                    <field name="TEXT">world</field>
+                    <field name="TEXT">banana</field>
                 </shadow>
             </value>
         </block>
@@ -592,26 +592,26 @@ const operators = function () {
             </value>
             <value name="STRING">
                 <shadow type="text">
-                    <field name="TEXT">world</field>
+                    <field name="TEXT">apple</field>
                 </shadow>
             </value>
         </block>
         <block type="operator_length">
             <value name="STRING">
                 <shadow type="text">
-                    <field name="TEXT">world</field>
+                    <field name="TEXT">apple</field>
                 </shadow>
             </value>
         </block>
         <block type="operator_contains" id="operator_contains">
           <value name="STRING1">
             <shadow type="text">
-              <field name="TEXT">hello</field>
+              <field name="TEXT">apple</field>
             </shadow>
           </value>
           <value name="STRING2">
             <shadow type="text">
-              <field name="TEXT">world</field>
+              <field name="TEXT">a</field>
             </shadow>
           </value>
         </block>
diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx
index 5ccb382b2560f27756bb04276ab7eaff3440e8a8..cad9367802759f35cec601e4eb89a4ede989503b 100644
--- a/src/lib/project-loader-hoc.jsx
+++ b/src/lib/project-loader-hoc.jsx
@@ -1,11 +1,13 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 
 import analytics from './analytics';
 import log from './log';
 import storage from './storage';
 
 /* Higher Order Component to provide behavior for loading projects by id from
- * the window's hash (#this part in the url)
+ * the window's hash (#this part in the url) or by projectId prop passed in from
+ * the parent (i.e. scratch-www)
  * @param {React.Component} WrappedComponent component to receive projectData prop
  * @returns {React.Component} component with project loading behavior
  */
@@ -45,7 +47,7 @@ const ProjectLoaderHOC = function (WrappedComponent) {
             return window.location.hash.substring(1);
         }
         updateProject () {
-            let projectId = this.fetchProjectId();
+            let projectId = this.props.projectId || this.fetchProjectId();
             if (projectId !== this.state.projectId) {
                 if (projectId.length < 1) projectId = 0;
                 this.setState({projectId: projectId});
@@ -61,21 +63,27 @@ const ProjectLoaderHOC = function (WrappedComponent) {
             }
         }
         render () {
+            const {
+                projectId, // eslint-disable-line no-unused-vars
+                ...componentProps
+            } = this.props;
             if (!this.state.projectData) return null;
             return (
                 <WrappedComponent
                     fetchingProject={this.state.fetchingProject}
                     projectData={this.state.projectData}
-                    {...this.props}
+                    {...componentProps}
                 />
             );
         }
     }
+    ProjectLoaderComponent.propTypes = {
+        projectId: PropTypes.string
+    };
 
     return ProjectLoaderComponent;
 };
 
-
 export {
     ProjectLoaderHOC as default
 };
diff --git a/src/examples/blocks-only.css b/src/playground/blocks-only.css
similarity index 100%
rename from src/examples/blocks-only.css
rename to src/playground/blocks-only.css
diff --git a/src/examples/blocks-only.jsx b/src/playground/blocks-only.jsx
similarity index 89%
rename from src/examples/blocks-only.jsx
rename to src/playground/blocks-only.jsx
index 8d5159e825105374827e818ce7009e57587e87de..af330d39d81c4de0a59e82c26a53275aa6e4b370 100644
--- a/src/examples/blocks-only.jsx
+++ b/src/playground/blocks-only.jsx
@@ -2,7 +2,6 @@ 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';
@@ -27,7 +26,7 @@ const BlocksOnly = props => (
     </GUI>
 );
 
-const App = AppStateHOC(ProjectLoaderHOC(BlocksOnly));
+const App = ProjectLoaderHOC(BlocksOnly);
 
 const appTarget = document.createElement('div');
 document.body.appendChild(appTarget);
diff --git a/src/examples/compatibility-testing.jsx b/src/playground/compatibility-testing.jsx
similarity index 95%
rename from src/examples/compatibility-testing.jsx
rename to src/playground/compatibility-testing.jsx
index 3fa8e71d94e34e4722ec1a1f329c9df46de2688c..b55a34c4ea6b0b70563a9f189284ae82f0019d30 100644
--- a/src/examples/compatibility-testing.jsx
+++ b/src/playground/compatibility-testing.jsx
@@ -2,7 +2,6 @@ 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';
@@ -72,7 +71,7 @@ class Player extends React.Component {
     }
 }
 
-const App = AppStateHOC(ProjectLoaderHOC(Player));
+const App = ProjectLoaderHOC(Player);
 
 const appTarget = document.createElement('div');
 document.body.appendChild(appTarget);
diff --git a/src/playground/error-boundary-hoc.jsx b/src/playground/error-boundary-hoc.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9132429851810454c402b9dc40d146201edcede2
--- /dev/null
+++ b/src/playground/error-boundary-hoc.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import ErrorBoundary from '../containers/error-boundary.jsx';
+
+/*
+ * Higher Order Component to provide error boundary for wrapped component
+ * @param {React.Component} WrappedComponent - component to provide state for
+ * @returns {React.Component} component with error boundary
+ */
+const ErrorBoundaryHOC = function (WrappedComponent) {
+    const ErrorBoundaryWrapper = props => (
+        <ErrorBoundary>
+            <WrappedComponent {...props} />
+        </ErrorBoundary>
+    );
+    return ErrorBoundaryWrapper;
+};
+
+export default ErrorBoundaryHOC;
diff --git a/src/index.css b/src/playground/index.css
similarity index 100%
rename from src/index.css
rename to src/playground/index.css
diff --git a/src/index.ejs b/src/playground/index.ejs
similarity index 100%
rename from src/index.ejs
rename to src/playground/index.ejs
diff --git a/src/index.jsx b/src/playground/index.jsx
similarity index 70%
rename from src/index.jsx
rename to src/playground/index.jsx
index 114c45ce0ad6194d76270bee6e79bea378ecb9ec..02ea975533d750449bd8c87c89e4ff96352ba5b6 100644
--- a/src/index.jsx
+++ b/src/playground/index.jsx
@@ -3,10 +3,9 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import Modal from 'react-modal';
 
-import analytics from './lib/analytics';
-import AppStateHOC from './lib/app-state-hoc.jsx';
-import GUI from './containers/gui.jsx';
-import ProjectLoaderHOC from './lib/project-loader-hoc.jsx';
+import analytics from '../lib/analytics';
+import GUI from '../containers/gui.jsx';
+import ErrorBoundaryHOC from './error-boundary-hoc.jsx';
 
 import styles from './index.css';
 
@@ -18,7 +17,7 @@ if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
 // Register "base" page view
 analytics.pageview('/');
 
-const App = AppStateHOC(ProjectLoaderHOC(GUI));
+const App = ErrorBoundaryHOC(GUI);
 
 const appTarget = document.createElement('div');
 appTarget.className = styles.app;
diff --git a/src/playground/intl.js b/src/playground/intl.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc427c402a022d03fa3bd64b0a6ffd6ff5b5afbf
--- /dev/null
+++ b/src/playground/intl.js
@@ -0,0 +1,25 @@
+import {addLocaleData} from 'react-intl';
+import defaultsDeep from 'lodash.defaultsdeep';
+
+import localeData from 'scratch-l10n';
+import guiMessages from 'scratch-l10n/locales/gui-msgs';
+import paintMessages from 'scratch-l10n/locales/paint-msgs';
+import penMessages from 'scratch-l10n/locales/pen-msgs';
+
+const combinedMessages = defaultsDeep({}, guiMessages.messages, paintMessages.messages, penMessages.messages);
+
+Object.keys(localeData).forEach(locale => {
+    // TODO: will need to handle locales not in the default intl - see www/custom-locales
+    addLocaleData(localeData[locale].localeData);
+});
+
+const intlDefault = {
+    defaultLocale: 'en',
+    locale: 'en',
+    messages: combinedMessages.en.messages
+};
+
+export {
+    intlDefault as default,
+    combinedMessages
+};
diff --git a/src/examples/player.css b/src/playground/player.css
similarity index 100%
rename from src/examples/player.css
rename to src/playground/player.css
diff --git a/src/examples/player.jsx b/src/playground/player.jsx
similarity index 95%
rename from src/examples/player.jsx
rename to src/playground/player.jsx
index ab50527cde1ae1d4be7788e7545397ff48b3700c..58bc0a6a73c7b65e5346fb4aa5eaddf63ce74e4d 100644
--- a/src/examples/player.jsx
+++ b/src/playground/player.jsx
@@ -2,7 +2,6 @@ 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';
@@ -69,7 +68,7 @@ class Player extends React.Component {
     }
 }
 
-const App = AppStateHOC(ProjectLoaderHOC(Player));
+const App = ProjectLoaderHOC(Player);
 
 const appTarget = document.createElement('div');
 document.body.appendChild(appTarget);
diff --git a/src/reducers/intl.js b/src/reducers/intl.js
index 15a2d307b885917cf75e363bf9b484d412edd2fb..8c49ee10475c5a960c35d6eba723203dbb772091 100644
--- a/src/reducers/intl.js
+++ b/src/reducers/intl.js
@@ -1,36 +1,14 @@
-import {addLocaleData} from 'react-intl';
-import {updateIntl as superUpdateIntl} from 'react-intl-redux';
-import {IntlProvider, intlReducer} from 'react-intl-redux';
-import defaultsDeep from 'lodash.defaultsdeep';
-
-import localeData from 'scratch-l10n';
-import guiMessages from 'scratch-l10n/locales/gui-msgs';
-import paintMessages from 'scratch-l10n/locales/paint-msgs';
-import penMessages from 'scratch-l10n/locales/pen-msgs';
-
-const combinedMessages = defaultsDeep({}, guiMessages.messages, paintMessages.messages, penMessages.messages);
-
-Object.keys(localeData).forEach(locale => {
-    // TODO: will need to handle locales not in the default intl - see www/custom-locales
-    addLocaleData(localeData[locale].localeData);
-});
+import {intlReducer} from 'react-intl-redux';
 
 const intlInitialState = {
     intl: {
         defaultLocale: 'en',
         locale: 'en',
-        messages: combinedMessages.en.messages
+        messages: {}
     }
 };
 
-const updateIntl = locale => superUpdateIntl({
-    locale: locale,
-    messages: combinedMessages[locale].messages || combinedMessages.en.messages
-});
-
 export {
     intlReducer as default,
-    IntlProvider,
-    intlInitialState,
-    updateIntl
+    intlInitialState
 };
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
index b18f356518c56c8154bc63b0acc9d856507e15f1..f88959e5478c9cdaf94bb8d5e32fccb5c4907294 100644
--- a/test/helpers/selenium-helper.js
+++ b/test/helpers/selenium-helper.js
@@ -6,6 +6,8 @@ import webdriver from 'selenium-webdriver';
 
 const {By, until, Button} = webdriver;
 
+const USE_HEADLESS = process.env.USE_HEADLESS !== 'no';
+
 class SeleniumHelper {
     constructor () {
         bindAll(this, [
@@ -35,7 +37,11 @@ class SeleniumHelper {
 
     getDriver () {
         const chromeCapabilities = webdriver.Capabilities.chrome();
-        chromeCapabilities.set('chromeOptions', {args: ['--headless']});
+        const args = [];
+        if (USE_HEADLESS) {
+            args.push('--headless');
+        }
+        chromeCapabilities.set('chromeOptions', {args});
         this.driver = new webdriver.Builder()
             .forBrowser('chrome')
             .withCapabilities(chromeCapabilities)
@@ -98,12 +104,8 @@ class SeleniumHelper {
                 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;
                     }
                 }
diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js
index 4f3411d5574521f651fbaad4b17ca553535629de..5fba5cfe4de9b26a1b32ae11bcfa58036c92883b 100644
--- a/test/integration/blocks.test.js
+++ b/test/integration/blocks.test.js
@@ -33,7 +33,7 @@ describe('Working with the blocks', () => {
         await clickText('Operators', scope.blocksTab);
         await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
         await clickText('join', scope.blocksTab); // Click "join <hello> <world>" block
-        await findByText('helloworld', scope.reportedValue); // Tooltip with result
+        await findByText('applebanana', scope.reportedValue); // Tooltip with result
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index bec4647e75b17b4c9535eb534d7fb6535adea585..bb4ee08c4b1958a98aaac1595742a3e3fe0d261c 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -30,7 +30,7 @@ describe('Working with costumes', () => {
         await clickXpath('//button[@title="tryit"]');
         await clickText('Costumes');
         await clickXpath('//button[@aria-label="Choose a Costume"]');
-        const el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        const el = await findByXpath("//input[@placeholder='Search']");
         await el.sendKeys('abb');
         await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector
         await findByXpath("//input[@value='Abby-a']"); // Should show editor for new costume
@@ -58,7 +58,7 @@ describe('Working with costumes', () => {
         await loadUri(uri);
         await clickXpath('//button[@title="tryit"]');
         await clickXpath('//button[@aria-label="Choose a Backdrop"]');
-        const el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        const el = await findByXpath("//input[@placeholder='Search']");
         await el.sendKeys('blue');
         await clickText('Blue Sky'); // Should close the modal
         const logs = await getLogs();
diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js
index 6add67c42b178055b653bf94cf9a29c41a5db9cc..cb0477ec9317a9fbb2fc0ca91c3e564b1b525ad2 100644
--- a/test/integration/project-loading.test.js
+++ b/test/integration/project-loading.test.js
@@ -66,6 +66,9 @@ describe('Loading scratch gui', () => {
         });
 
         test('Load a project by ID directly through url', async () => {
+            await driver.quit(); // Reset driver to test hitting # url directly
+            driver = getDriver();
+
             const projectId = '96708228';
             await loadUri(`${uri}#${projectId}`);
             await clickXpath('//button[@title="tryit"]');
@@ -78,6 +81,9 @@ describe('Loading scratch gui', () => {
         });
 
         test('Load a project by ID (fullscreen)', async () => {
+            await driver.quit(); // Reset driver to test hitting # url directly
+            driver = getDriver();
+
             const prevSize = driver.manage()
                 .window()
                 .getSize();
diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js
index 2cbff5d84bc7a6f9314eeab0950d5fb202ed2781..44b670dd72c51ccb74a7dc6477541df21bbb70c4 100644
--- a/test/integration/sounds.test.js
+++ b/test/integration/sounds.test.js
@@ -38,13 +38,13 @@ describe('Working with sounds', () => {
 
         // Add it back
         await clickXpath('//button[@aria-label="Choose a Sound"]');
-        let el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        let el = await findByXpath("//input[@placeholder='Search']");
         await el.sendKeys('meow');
         await clickText('Meow', scope.modal); // Should close the modal
 
         // Add a new sound
         await clickXpath('//button[@aria-label="Choose a Sound"]');
-        el = await findByXpath("//input[@placeholder='what are you looking for?']");
+        el = await findByXpath("//input[@placeholder='Search']");
         await el.sendKeys('chom');
         await clickText('Chomp'); // Should close the modal, then click the sounds in the selector
         await findByXpath("//input[@value='Chomp']"); // Should show editor for new sound
diff --git a/webpack.config.js b/webpack.config.js
index 389cf8abd6db12e1deb277d680469a688d518cb6..393e9dbb44ffe9b25971ad3d7195dd319ad6e977 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,3 +1,4 @@
+const defaultsDeep = require('lodash.defaultsdeep');
 var path = require('path');
 var webpack = require('webpack');
 
@@ -10,22 +11,15 @@ var autoprefixer = require('autoprefixer');
 var postcssVars = require('postcss-simple-vars');
 var postcssImport = require('postcss-import');
 
-module.exports = {
+const base = {
+    devtool: 'cheap-module-source-map',
     devServer: {
         contentBase: path.resolve(__dirname, 'build'),
         host: '0.0.0.0',
         port: process.env.PORT || 8601
     },
-    devtool: 'cheap-module-source-map',
-    entry: {
-        lib: ['react', 'react-dom'],
-        gui: './src/index.jsx',
-        blocksonly: './src/examples/blocks-only.jsx',
-        compatibilitytesting: './src/examples/compatibility-testing.jsx',
-        player: './src/examples/player.jsx'
-    },
     output: {
-        path: path.resolve(__dirname, 'build'),
+        library: 'GUI',
         filename: '[name].js'
     },
     externals: {
@@ -65,65 +59,133 @@ module.exports = {
                     }
                 }
             }]
-        },
-        {
-            test: /\.(svg|png|wav)$/,
-            loader: 'file-loader'
         }]
     },
-    plugins: [
-        new webpack.DefinePlugin({
-            'process.env.NODE_ENV': '"' + process.env.NODE_ENV + '"',
-            'process.env.DEBUG': Boolean(process.env.DEBUG)
-        }),
-        new webpack.optimize.CommonsChunkPlugin({
-            name: 'lib',
-            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', 'compatibilitytesting'],
-            template: 'src/index.ejs',
-            filename: 'compatibility-testing.html',
-            title: 'Scratch 3.0 GUI: Compatibility Testing'
-        }),
-        new HtmlWebpackPlugin({
-            chunks: ['lib', 'player'],
-            template: 'src/index.ejs',
-            filename: 'player.html',
-            title: 'Scratch 3.0 GUI: Player Example'
-        }),
-        new CopyWebpackPlugin([{
-            from: 'static',
-            to: 'static'
-        }]),
-        new CopyWebpackPlugin([{
-            from: 'node_modules/scratch-blocks/media',
-            to: 'static/blocks-media'
-        }]),
-        new CopyWebpackPlugin([{
-            from: 'extensions/**',
-            to: 'static',
-            context: 'src/examples'
-        }]),
-        new CopyWebpackPlugin([{
-            from: 'extension-worker.{js,js.map}',
-            context: 'node_modules/scratch-vm/dist/web'
-        }])
-    ].concat(process.env.NODE_ENV === 'production' ? [
+    plugins: [].concat(process.env.NODE_ENV === 'production' ? [
         new webpack.optimize.UglifyJsPlugin({
             include: /\.min\.js$/,
             minimize: true
         })
     ] : [])
 };
+
+module.exports = [
+    // to run editor examples
+    defaultsDeep({}, base, {
+        entry: {
+            lib: ['react', 'react-dom'],
+            gui: './src/playground/index.jsx',
+            blocksonly: './src/playground/blocks-only.jsx',
+            compatibilitytesting: './src/playground/compatibility-testing.jsx',
+            player: './src/playground/player.jsx'
+        },
+        output: {
+            path: path.resolve(__dirname, 'build'),
+            filename: '[name].js'
+        },
+        externals: {
+            React: 'react',
+            ReactDOM: 'react-dom'
+        },
+        module: {
+            rules: base.module.rules.concat([
+                {
+                    test: /\.(svg|png|wav)$/,
+                    loader: 'file-loader',
+                    options: {
+                        outputPath: 'static/assets/'
+                    }
+                }
+            ])
+        },
+        plugins: base.plugins.concat([
+            new webpack.DefinePlugin({
+                'process.env.NODE_ENV': '"' + process.env.NODE_ENV + '"',
+                'process.env.DEBUG': Boolean(process.env.DEBUG)
+            }),
+            new webpack.optimize.CommonsChunkPlugin({
+                name: 'lib',
+                filename: 'lib.min.js'
+            }),
+            new HtmlWebpackPlugin({
+                chunks: ['lib', 'gui'],
+                template: 'src/playground/index.ejs',
+                title: 'Scratch 3.0 GUI'
+            }),
+            new HtmlWebpackPlugin({
+                chunks: ['lib', 'blocksonly'],
+                template: 'src/playground/index.ejs',
+                filename: 'blocks-only.html',
+                title: 'Scratch 3.0 GUI: Blocks Only Example'
+            }),
+            new HtmlWebpackPlugin({
+                chunks: ['lib', 'compatibilitytesting'],
+                template: 'src/playground/index.ejs',
+                filename: 'compatibility-testing.html',
+                title: 'Scratch 3.0 GUI: Compatibility Testing'
+            }),
+            new HtmlWebpackPlugin({
+                chunks: ['lib', 'player'],
+                template: 'src/playground/index.ejs',
+                filename: 'player.html',
+                title: 'Scratch 3.0 GUI: Player Example'
+            }),
+            new CopyWebpackPlugin([{
+                from: 'static',
+                to: 'static'
+            }]),
+            new CopyWebpackPlugin([{
+                from: 'node_modules/scratch-blocks/media',
+                to: 'static/blocks-media'
+            }]),
+            new CopyWebpackPlugin([{
+                from: 'extensions/**',
+                to: 'static',
+                context: 'src/examples'
+            }]),
+            new CopyWebpackPlugin([{
+                from: 'extension-worker.{js,js.map}',
+                context: 'node_modules/scratch-vm/dist/web'
+            }])
+        ])
+    })
+].concat(
+    process.env.NODE_ENV === 'production' ? (
+        // export as library
+        defaultsDeep({}, base, {
+            target: 'web',
+            entry: {
+                'scratch-gui': './src/containers/gui.jsx'
+            },
+            output: {
+                libraryTarget: 'umd',
+                path: path.resolve('dist')
+            },
+            externals: {
+                React: 'react',
+                ReactDOM: 'react-dom'
+            },
+            module: {
+                rules: base.module.rules.concat([
+                    {
+                        test: /\.(svg|png|wav)$/,
+                        loader: 'file-loader',
+                        options: {
+                            outputPath: 'static/assets/',
+                            publicPath: '/static/assets/'
+                        }
+                    }
+                ])
+            },
+            plugins: base.plugins.concat([
+                new CopyWebpackPlugin([{
+                    from: 'node_modules/scratch-blocks/media',
+                    to: 'static/blocks-media'
+                }]),
+                new CopyWebpackPlugin([{
+                    from: 'extension-worker.{js,js.map}',
+                    context: 'node_modules/scratch-vm/dist/web'
+                }])
+            ])
+        })) : []
+);