diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css
new file mode 100644
index 0000000000000000000000000000000000000000..5f631e41bf5ad9b22675065153f683534c896a7b
--- /dev/null
+++ b/src/components/camera-modal/camera-modal.css
@@ -0,0 +1,153 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+
+$main-button-size: 2.75rem;
+
+.modal-content {
+    width: 552px;
+}
+
+.body {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: $ui-white;
+    padding: 1.5rem 2.25rem;
+}
+
+.camera-feed-container {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+
+    background: $ui-primary;
+    border: 1px solid $ui-black-transparent;
+    border-radius: 4px;
+    padding: 3px;
+
+    width: 480px;
+    height: 360px;
+    position: relative;
+    overflow: hidden;
+}
+
+.canvas {
+    position: absolute;
+    width: 480px;
+    height: 360px;
+}
+
+.loading-text {
+    position: absolute;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    color: $text-primary-transparent;
+    font-size: 0.95rem;
+    font-weight: 500;
+    text-align: center;
+}
+
+.help-text {
+    margin: 10px auto 0;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    color: $text-primary-transparent;
+    font-size: 0.95rem;
+    font-weight: 500;
+    text-align: center;
+}
+
+.capture-text {
+    color: $motion-primary;
+}
+
+.disabled-text {
+    color: $text-primary;
+    opacity: 0.25;
+}
+
+.main-button-row {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-around;
+    margin-top: 15px;
+    width: 100%;
+}
+
+/* Action Menu */
+.main-button {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    background: $motion-primary;
+    outline: none;
+    border: none;
+    transition: background-color 0.2s;
+
+    border-radius: 100%;
+    width: $main-button-size;
+    height: $main-button-size;
+    box-shadow: 0 0 0 4px $motion-transparent;
+}
+
+.main-button:hover {
+    background: $pen-primary;
+    box-shadow: 0 0 0 6px $motion-transparent;
+}
+
+.main-button:disabled {
+    background: $text-primary;
+    border-color: $ui-black-transparent;
+    box-shadow: none;
+    opacity: 0.25;
+}
+
+.main-icon {
+    width: calc($main-button-size - 1rem);
+    height: calc($main-button-size - 1rem);
+}
+
+.button-row {
+    font-weight: bolder;
+    text-align: right;
+    display: flex;
+    justify-content: space-between;
+    margin-top: 20px;
+    width: 480px;
+}
+
+.button-row button {
+    padding: 0.75rem 1rem;
+    border-radius: 0.25rem;
+    background: $ui-white;
+    border: 1px solid $ui-black-transparent;
+    font-weight: 600;
+    font-size: 0.85rem;
+    color: $motion-primary;
+    cursor: pointer;
+}
+
+.button-row button.ok-button {
+    background: $motion-primary;
+    border: $motion-primary;
+    color: $ui-white;
+}
+
+@keyframes flash {
+    0% { opacity: 1; }
+    100% { opacity: 0; }
+}
+
+.flash-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: $ui-white;
+    animation-name: flash;
+    animation-duration: 0.5s;
+    animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
+}
diff --git a/src/components/camera-modal/camera-modal.jsx b/src/components/camera-modal/camera-modal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f115a6f8a75f1c77286f15365137509313a8783f
--- /dev/null
+++ b/src/components/camera-modal/camera-modal.jsx
@@ -0,0 +1,141 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
+import Box from '../box/box.jsx';
+import Modal from '../modal/modal.jsx';
+import styles from './camera-modal.css';
+import backIcon from './icon--back.svg';
+import cameraIcon from '../action-menu/icon--camera.svg';
+
+const messages = defineMessages({
+    cameraModalTitle: {
+        defaultMessage: 'Take a Photo',
+        description: 'Title for prompt to take a picture (to add as a new costume).',
+        id: 'gui.cameraModal.cameraModalTitle'
+    },
+    loadingCameraMessage: {
+        defaultMessage: 'Loading Camera...',
+        description: 'Notification to the user that the camera is loading',
+        id: 'gui.cameraModal.loadingCameraMessage'
+    },
+    permissionRequest: {
+        defaultMessage: 'We need your permission to use your camera',
+        description: 'Notification to the user that the app needs camera access',
+        id: 'gui.cameraModal.permissionRequest'
+    },
+    retakePhoto: {
+        defaultMessage: 'Retake Photo',
+        description: 'A button that allows the user to take the picture again, replacing the old one',
+        id: 'gui.cameraModal.retakePhoto'
+    },
+    save: {
+        defaultMessage: 'Save',
+        description: 'A button that allows the user to save the photo they took as a costume',
+        id: 'gui.cameraModal.save'
+    },
+    takePhotoButton: {
+        defaultMessage: 'Take Photo',
+        description: 'A button to take a photo',
+        id: 'gui.cameraModal.takePhoto'
+    },
+    loadingCaption: {
+        defaultMessage: 'Loading...',
+        description: 'A caption for a disabled button while the video from the camera is still loading',
+        id: 'gui.cameraModal.loadingCaption'
+    },
+    enableCameraCaption: {
+        defaultMessage: 'Enable Camera',
+        description: 'A caption for a disabled button prompting the user to enable camera access',
+        id: 'gui.cameraModal.enableCameraCaption'
+    }
+});
+
+const CameraModal = ({intl, ...props}) => (
+    <Modal
+        className={styles.modalContent}
+        contentLabel={intl.formatMessage(messages.cameraModalTitle)}
+        onRequestClose={props.onCancel}
+    >
+        <Box className={styles.body}>
+            <Box className={styles.cameraFeedContainer}>
+                <div className={styles.loadingText}>
+                    {props.access ? intl.formatMessage(messages.loadingCameraMessage) :
+                        `↖️ \u00A0${intl.formatMessage(messages.permissionRequest)}`}
+                </div>
+                <canvas
+                    className={styles.canvas}
+                    // height and (below) width of the actual image
+                    // double stage dimensions to avoid the need for
+                    // resizing the captured image when importing costume
+                    // to accommodate double resolution bitmaps
+                    height="720"
+                    ref={props.canvasRef}
+                    width="960"
+                />
+                {props.capture ? (
+                    <div className={styles.flashOverlay} />
+                ) : null}
+            </Box>
+            {props.capture ?
+                <Box className={styles.buttonRow}>
+                    <button
+                        className={styles.cancelButton}
+                        key="retake-button"
+                        onClick={props.onBack}
+                    >
+                        <img
+                            draggable={false}
+                            src={backIcon}
+                        /> {intl.formatMessage(messages.retakePhoto)}
+                    </button>
+                    <button
+                        className={styles.okButton}
+                        onClick={props.onSubmit}
+                    > {intl.formatMessage(messages.save)}
+                    </button>
+                </Box> :
+                <Box className={styles.mainButtonRow}>
+                    <button
+                        className={styles.mainButton}
+                        disabled={!props.loaded}
+                        key="capture-button"
+                        onClick={props.onCapture}
+                    >
+                        <img
+                            className={styles.mainIcon}
+                            draggable={false}
+                            src={cameraIcon}
+                        />
+                    </button>
+                    <div className={styles.helpText}>
+                        {props.access ?
+                            <span className={props.loaded ? styles.captureText : styles.disabledText}>
+                                {props.loaded ?
+                                    intl.formatMessage(messages.takePhotoButton) :
+                                    intl.formatMessage(messages.loadingCaption)}
+                            </span> :
+                            <span className={styles.disabledText}>
+                                {intl.formatMessage(messages.enableCameraCaption)}
+                            </span>
+                        }
+                    </div>
+
+                </Box>
+            }
+        </Box>
+    </Modal>
+);
+
+CameraModal.propTypes = {
+    access: PropTypes.bool,
+    canvasRef: PropTypes.func.isRequired,
+    capture: PropTypes.string,
+    intl: intlShape.isRequired,
+    loaded: PropTypes.bool,
+    onBack: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onCapture: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired
+};
+
+export default injectIntl(CameraModal);
diff --git a/src/components/camera-modal/icon--back.svg b/src/components/camera-modal/icon--back.svg
new file mode 100644
index 0000000000000000000000000000000000000000..47d09bc7fad766b50da3ed1cffec190b1e94878c
Binary files /dev/null and b/src/components/camera-modal/icon--back.svg differ
diff --git a/src/containers/camera-modal.jsx b/src/containers/camera-modal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c55b0ee450797d7d72e93343e7d5df4d6a7ceeda
--- /dev/null
+++ b/src/containers/camera-modal.jsx
@@ -0,0 +1,103 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {connect} from 'react-redux';
+
+import CameraModalComponent from '../components/camera-modal/camera-modal.jsx';
+import ModalVideoManager from '../lib/video/modal-video-manager.js';
+
+import {
+    closeCameraCapture
+} from '../reducers/modals';
+
+class CameraModal extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleAccess',
+            'handleBack',
+            'handleCancel',
+            'handleCapture',
+            'handleLoaded',
+            'handleSubmit',
+            'setCanvas'
+        ]);
+
+        this.video = null;
+        this.videoDevice = null;
+
+        this.state = {
+            capture: null,
+            access: false,
+            loaded: false
+        };
+    }
+    componentWillUnmount () {
+        if (this.videoDevice) {
+            this.videoDevice.disableVideo();
+        }
+    }
+    handleAccess () {
+        this.setState({access: true});
+    }
+    handleLoaded () {
+        this.setState({loaded: true});
+    }
+    handleBack () {
+        this.setState({capture: null});
+        this.videoDevice.clearSnapshot();
+    }
+    handleCapture () {
+        if (this.state.loaded) {
+            const capture = this.videoDevice.takeSnapshot();
+            this.setState({capture: capture});
+        }
+    }
+    setCanvas (canvas) {
+        this.canvas = canvas;
+        if (this.canvas) {
+            this.videoDevice = new ModalVideoManager(this.canvas);
+            this.videoDevice.enableVideo(this.handleAccess, this.handleLoaded);
+        }
+    }
+    handleSubmit () {
+        if (!this.state.capture) return;
+        this.props.onNewCostume(this.state.capture);
+        this.props.onClose();
+    }
+    handleCancel () {
+        this.props.onClose();
+    }
+    render () {
+        return (
+            <CameraModalComponent
+                access={this.state.access}
+                canvasRef={this.setCanvas}
+                capture={this.state.capture}
+                loaded={this.state.loaded}
+                onBack={this.handleBack}
+                onCancel={this.handleCancel}
+                onCapture={this.handleCapture}
+                onSubmit={this.handleSubmit}
+            />
+        );
+    }
+}
+
+CameraModal.propTypes = {
+    onClose: PropTypes.func,
+    onNewCostume: PropTypes.func
+};
+
+const mapStateToProps = () => ({});
+
+const mapDispatchToProps = dispatch => ({
+    onClose: () => {
+        dispatch(closeCameraCapture());
+    }
+});
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(CameraModal);
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 6042ceaaac8003eb03e4594faa2a91eb5b563b77..641d77380d01d254d91b3f95ac3b38234e91dd24 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -8,12 +8,15 @@ import AssetPanel from '../components/asset-panel/asset-panel.jsx';
 import PaintEditorWrapper from './paint-editor-wrapper.jsx';
 import CostumeLibrary from './costume-library.jsx';
 import BackdropLibrary from './backdrop-library.jsx';
+import CameraModal from './camera-modal.jsx';
 import {connect} from 'react-redux';
 import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
 
 import {
+    closeCameraCapture,
     closeCostumeLibrary,
     closeBackdropLibrary,
+    openCameraCapture,
     openCostumeLibrary,
     openBackdropLibrary
 } from '../reducers/modals';
@@ -60,7 +63,7 @@ const messages = defineMessages({
         id: 'gui.costumeTab.addFileCostume'
     },
     addCameraCostumeMsg: {
-        defaultMessage: 'Coming Soon',
+        defaultMessage: 'Camera',
         description: 'Button to use the camera to create a costume costume in the editor tab',
         id: 'gui.costumeTab.addCameraCostume'
     }
@@ -79,6 +82,7 @@ class CostumeTab extends React.Component {
             'handleSurpriseBackdrop',
             'handleFileUploadClick',
             'handleCostumeUpload',
+            'handleCameraBuffer',
             'setFileInput'
         ]);
         const {
@@ -180,6 +184,10 @@ class CostumeTab extends React.Component {
             costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume);
         });
     }
+    handleCameraBuffer (buffer) {
+        const storage = this.props.vm.runtime.storage;
+        costumeUpload(buffer, 'image/png', 'costume1', storage, this.handleNewCostume);
+    }
     handleFileUploadClick () {
         this.fileInput.click();
     }
@@ -194,11 +202,14 @@ class CostumeTab extends React.Component {
     render () {
         const {
             intl,
+            onNewCostumeFromCameraClick,
             onNewLibraryBackdropClick,
             onNewLibraryCostumeClick,
             backdropLibraryVisible,
+            cameraModalVisible,
             costumeLibraryVisible,
             onRequestCloseBackdropLibrary,
+            onRequestCloseCameraModal,
             onRequestCloseCostumeLibrary,
             editingTarget,
             sprites,
@@ -234,13 +245,14 @@ class CostumeTab extends React.Component {
                     },
                     {
                         title: intl.formatMessage(messages.addCameraCostumeMsg),
-                        img: cameraIcon
+                        img: cameraIcon,
+                        onClick: onNewCostumeFromCameraClick
                     },
                     {
                         title: intl.formatMessage(addFileMessage),
                         img: fileUploadIcon,
                         onClick: this.handleFileUploadClick,
-                        fileAccept: '.svg, .png, .jpg, .jpeg', // coming soon
+                        fileAccept: '.svg, .png, .jpg, .jpeg',
                         fileChange: this.handleCostumeUpload,
                         fileInput: this.setFileInput
                     },
@@ -280,6 +292,12 @@ class CostumeTab extends React.Component {
                         onRequestClose={onRequestCloseBackdropLibrary}
                     />
                 ) : null}
+                {cameraModalVisible ? (
+                    <CameraModal
+                        onClose={onRequestCloseCameraModal}
+                        onNewCostume={this.handleCameraBuffer}
+                    />
+                ) : null}
             </AssetPanel>
         );
     }
@@ -287,12 +305,15 @@ class CostumeTab extends React.Component {
 
 CostumeTab.propTypes = {
     backdropLibraryVisible: PropTypes.bool,
+    cameraModalVisible: PropTypes.bool,
     costumeLibraryVisible: PropTypes.bool,
     editingTarget: PropTypes.string,
     intl: intlShape,
+    onNewCostumeFromCameraClick: PropTypes.func.isRequired,
     onNewLibraryBackdropClick: PropTypes.func.isRequired,
     onNewLibraryCostumeClick: PropTypes.func.isRequired,
     onRequestCloseBackdropLibrary: PropTypes.func.isRequired,
+    onRequestCloseCameraModal: PropTypes.func.isRequired,
     onRequestCloseCostumeLibrary: PropTypes.func.isRequired,
     sprites: PropTypes.shape({
         id: PropTypes.shape({
@@ -315,6 +336,7 @@ const mapStateToProps = state => ({
     editingTarget: state.targets.editingTarget,
     sprites: state.targets.sprites,
     stage: state.targets.stage,
+    cameraModalVisible: state.modals.cameraCapture,
     costumeLibraryVisible: state.modals.costumeLibrary,
     backdropLibraryVisible: state.modals.backdropLibrary
 });
@@ -328,11 +350,17 @@ const mapDispatchToProps = dispatch => ({
         e.preventDefault();
         dispatch(openCostumeLibrary());
     },
+    onNewCostumeFromCameraClick: () => {
+        dispatch(openCameraCapture());
+    },
     onRequestCloseBackdropLibrary: () => {
         dispatch(closeBackdropLibrary());
     },
     onRequestCloseCostumeLibrary: () => {
         dispatch(closeCostumeLibrary());
+    },
+    onRequestCloseCameraModal: () => {
+        dispatch(closeCameraCapture());
     }
 });
 
diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js
index 040a026b258ab50083a740270aceab82d929bcf3..ca4ce8773c113b92ef9fcfb460daeda609242482 100644
--- a/src/lib/file-uploader.js
+++ b/src/lib/file-uploader.js
@@ -77,7 +77,8 @@ const cacheAsset = function (storage, fileName, assetType, dataFormat, data) {
 
 /**
  * Handles loading a costume or a backdrop using the provided, context-relevant information.
- * @param {ArrayBuffer} fileData The costume data to load
+ * @param {ArrayBuffer | string} fileData The costume data to load (this can be an image url
+ * iff the image is a bitmap)
  * @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
diff --git a/src/reducers/modals.js b/src/reducers/modals.js
index 481c44f29e520c379ef911be714cbc2690c43e0d..c373ce79610eb53010f5dc45b22ec3f6f3fc0390 100644
--- a/src/reducers/modals.js
+++ b/src/reducers/modals.js
@@ -4,6 +4,7 @@ const OPEN_MODAL = 'scratch-gui/modals/OPEN_MODAL';
 const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL';
 
 const MODAL_BACKDROP_LIBRARY = 'backdropLibrary';
+const MODAL_CAMERA_CAPTURE = 'cameraCapture';
 const MODAL_COSTUME_LIBRARY = 'costumeLibrary';
 const MODAL_EXTENSION_LIBRARY = 'extensionLibrary';
 const MODAL_IMPORT_INFO = 'importInfo';
@@ -18,6 +19,7 @@ const MODAL_TIPS_LIBRARY = 'tipsLibrary';
 
 const initialState = {
     [MODAL_BACKDROP_LIBRARY]: false,
+    [MODAL_CAMERA_CAPTURE]: false,
     [MODAL_COSTUME_LIBRARY]: false,
     [MODAL_EXTENSION_LIBRARY]: false,
     [MODAL_IMPORT_INFO]: false,
@@ -60,6 +62,10 @@ const openBackdropLibrary = function () {
     analytics.pageview('/libraries/backdrops');
     return openModal(MODAL_BACKDROP_LIBRARY);
 };
+const openCameraCapture = function () {
+    analytics.pageview('/modals/camera');
+    return openModal(MODAL_CAMERA_CAPTURE);
+};
 const openCostumeLibrary = function () {
     analytics.pageview('/libraries/costumes');
     return openModal(MODAL_COSTUME_LIBRARY);
@@ -99,6 +105,9 @@ const openTipsLibrary = function () {
 const closeBackdropLibrary = function () {
     return closeModal(MODAL_BACKDROP_LIBRARY);
 };
+const closeCameraCapture = function () {
+    return closeModal(MODAL_CAMERA_CAPTURE);
+};
 const closeCostumeLibrary = function () {
     return closeModal(MODAL_COSTUME_LIBRARY);
 };
@@ -129,6 +138,7 @@ const closeTipsLibrary = function () {
 export {
     reducer as default,
     openBackdropLibrary,
+    openCameraCapture,
     openCostumeLibrary,
     openExtensionLibrary,
     openImportInfo,
@@ -139,6 +149,7 @@ export {
     openSoundRecorder,
     openTipsLibrary,
     closeBackdropLibrary,
+    closeCameraCapture,
     closeCostumeLibrary,
     closeExtensionLibrary,
     closeImportInfo,