diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c02f644bc4e55d807124bd8cc41004a98cdca24b
--- /dev/null
+++ b/src/components/connection-modal/connected-step.jsx
@@ -0,0 +1,71 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Box from '../box/box.jsx';
+import Dots from './dots.jsx';
+import bluetoothIcon from './icons/bluetooth-white.svg';
+import styles from './connection-modal.css';
+import classNames from 'classnames';
+
+const ConnectedStep = props => (
+    <Box className={styles.body}>
+        <Box className={styles.activityArea}>
+            <Box className={styles.centeredRow}>
+                <div className={styles.deviceActivity}>
+                    <img
+                        className={styles.deviceActivityIcon}
+                        src={props.deviceImage}
+                    />
+                    <img
+                        className={styles.bluetoothConnectedIcon}
+                        src={bluetoothIcon}
+                    />
+                </div>
+            </Box>
+        </Box>
+        <Box className={styles.bottomArea}>
+            <Box className={styles.instructions}>
+                <FormattedMessage
+                    defaultMessage="Connected"
+                    description="Message indicating that a device was connected"
+                    id="gui.connection.connected"
+                />
+            </Box>
+            <Dots
+                success
+                total={3}
+            />
+            <div className={styles.cornerButtons}>
+                <button
+                    className={classNames(styles.redButton, styles.connectionButton)}
+                    onClick={props.onDisconnect}
+                >
+                    <FormattedMessage
+                        defaultMessage="Disconnect"
+                        description="Button to disconnect the device"
+                        id="gui.connection.disconnect"
+                    />
+                </button>
+                <button
+                    className={styles.connectionButton}
+                    onClick={props.onCancel}
+                >
+                    <FormattedMessage
+                        defaultMessage="Go to Editor"
+                        description="Button to return to the editor"
+                        id="gui.connection.go-to-editor"
+                    />
+                </button>
+            </div>
+        </Box>
+    </Box>
+);
+
+ConnectedStep.propTypes = {
+    deviceImage: PropTypes.string.isRequired,
+    onCancel: PropTypes.func,
+    onDisconnect: PropTypes.func
+};
+
+export default ConnectedStep;
diff --git a/src/components/connection-modal/connecting-step.jsx b/src/components/connection-modal/connecting-step.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..063e67a826c79f4c9f55548e321d499d8c86d6a3
--- /dev/null
+++ b/src/components/connection-modal/connecting-step.jsx
@@ -0,0 +1,71 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Box from '../box/box.jsx';
+import Dots from './dots.jsx';
+
+import bluetoothIcon from './icons/bluetooth-white.svg';
+import closeIcon from '../close-button/icon--close.svg';
+
+import styles from './connection-modal.css';
+
+const ConnectingStep = props => (
+    <Box className={styles.body}>
+        <Box className={styles.activityArea}>
+            <Box className={styles.centeredRow}>
+                <div className={styles.deviceActivity}>
+                    <img
+                        className={styles.deviceActivityIcon}
+                        src={props.deviceImage}
+                    />
+                    <img
+                        className={styles.bluetoothConnectingIcon}
+                        src={bluetoothIcon}
+                    />
+                </div>
+            </Box>
+        </Box>
+        <Box className={styles.bottomArea}>
+            <Box className={styles.instructions}>
+                <FormattedMessage
+                    defaultMessage="Connecting"
+                    description=""
+                    id="gui.connection.connecting"
+                />
+            </Box>
+            <Dots
+                counter={1}
+                total={3}
+            />
+            <div className={styles.segmentedButton}>
+                <button
+                    disabled
+                    className={styles.connectionButton}
+                >
+                    <FormattedMessage
+                        defaultMessage="Connecting..."
+                        description="Label indicating that connection is in progress"
+                        id="gui.connection.connecting-cancelbutton"
+                    />
+                </button>
+                <button
+                    className={styles.connectionButton}
+                    onClick={props.onDisconnect}
+                >
+                    <img
+                        className={styles.abortConnectingIcon}
+                        src={closeIcon}
+                    />
+                </button>
+            </div>
+        </Box>
+    </Box>
+);
+
+ConnectingStep.propTypes = {
+    deviceImage: PropTypes.string.isRequired,
+    onDisconnect: PropTypes.func
+};
+
+export default ConnectingStep;
diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css
new file mode 100644
index 0000000000000000000000000000000000000000..ee0a0aae1e6ee354f7c3a2decd302f65b000644d
--- /dev/null
+++ b/src/components/connection-modal/connection-modal.css
@@ -0,0 +1,308 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+
+.modal-content {
+    width: 480px;
+}
+
+.header {
+    background-color: $pen-primary;
+}
+
+.body {
+    background: $ui-white;
+}
+
+.label {
+    font-weight: 500;
+    margin: 0 0 0.75rem;
+}
+
+.centered-row {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+}
+
+.device-tile-pane {
+    overflow-y: auto;
+    width: 100%;
+    height: 100%;
+    padding: 0.5rem;
+}
+
+.device-tile {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+
+    background-color: $ui-white;
+    border-radius: 0.25rem;
+    padding: 10px;
+    width: 100%;
+    height: 55px;
+    margin-bottom: 0.5rem;
+}
+
+.device-tile-name {
+    display: flex;
+    align-items: center;
+}
+
+.device-tile-image {
+    margin-right: 0.5rem;
+}
+
+.device-tile-name-wrapper {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+.device-tile-name-label {
+    font-weight: bold;
+    font-size: 0.625rem;
+}
+
+.device-tile-name-text {
+    font-size: 0.875rem;
+}
+
+.device-tile button {
+    padding: 0.6rem 0.75rem;
+    border: none;
+    border-radius: 0.25rem;
+    font-weight: 600;
+    font-size: 0.85rem;
+    background: $motion-primary;
+    border: $motion-primary;
+    color: white;
+    cursor: pointer;
+}
+
+.signal-strength-meter {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+    width: 22px;
+    height: 16px;
+    margin-right: 1rem;
+}
+
+.signal-bar {
+    width: 4px;
+    border-radius: 4px;
+    background-color: #DBDBDB;
+}
+
+.signal-bar:nth-of-type(1) { height: 25%; }
+.signal-bar:nth-of-type(2) { height: 50%; }
+.signal-bar:nth-of-type(3) { height: 75%; }
+.signal-bar:nth-of-type(4) { height: 100%; }
+
+.green-bar {
+    background-color: $pen-primary;
+}
+
+.radar {
+    width: 40px;
+    height: 40px;
+    margin-right: 0.5rem;
+    animation: spin 4s linear infinite;
+}
+
+@keyframes spin {
+    100% {
+        transform: rotate(360deg);
+    }
+}
+
+
+.device-activity {
+    position: relative;
+}
+
+.device-activity-icon {
+    /* width: 80px;
+    height: 80px; */
+}
+
+.bluetooth-connecting-icon {
+    position: absolute;
+    top: -5px;
+    right: -15px;
+    padding: 5px 5px;
+    background-color: $motion-primary;
+    border-radius: 100%;
+    box-shadow: 0px 0px 0px 4px $motion-transparent;
+    /* animation: pulse-blue-ring 1s infinite ease-in-out alternate; */
+    animation: wiggle 0.5s infinite ease-in-out alternate;
+
+}
+
+@keyframes pulse-blue-ring {
+    100% {
+        box-shadow: 0px 0px 0px 8px $motion-light-transparent;
+    }
+}
+
+
+.bluetooth-connected-icon {
+    position: absolute;
+    top: -5px;
+    right: -15px;
+    padding: 5px 5px;
+    background-color: $pen-primary;
+    border-radius: 100%;
+    box-shadow: 0px 0px 0px 4px $pen-transparent;
+}
+
+
+
+@keyframes wiggle {
+    0% {transform: rotate(3deg) scale(1.05);}
+    25% {transform: rotate(-3deg) scale(1.05);}
+    50% {transform: rotate(5deg) scale(1.05);}
+    75% {transform: rotate(-2deg) scale(1.05);}
+    100% {transform: rotate(0deg) scale(1.05);}
+}
+
+.device-tile-widgets {
+    display: flex;
+    align-items: center;
+}
+
+.activityArea {
+    height: 165px;
+    background-color: $motion-light-transparent;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.button-row {
+    font-weight: bolder;
+    text-align: center;
+    display: flex;
+}
+
+.abort-connecting-icon {
+    width: 10px;
+    transform: rotate(45deg);
+}
+
+.connection-button {
+    padding: 0.6rem 0.75rem;
+    border-radius: 0.5rem;
+    background: $motion-primary;
+    color: white;
+    font-weight: 600;
+    font-size: 0.85rem;
+    margin: 0.25rem;
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+}
+
+.connection-button:disabled {
+    background: $motion-transparent;
+}
+
+.segmented-button {
+    display: flex;
+}
+
+.segmented-button .connection-button:first-of-type {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    margin-right: 0;
+}
+
+.segmented-button .connection-button:last-of-type {
+    margin-left: 1px;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+.button-icon-right {
+    margin-left: 0.5rem;
+}
+
+.button-icon-left {
+    margin-right: 0.5rem;
+}
+
+.red-button {
+    background: $red-primary;
+}
+
+.corner-buttons {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    padding: 0 1rem;
+}
+
+.bottom-area {
+    background-color: $ui-white;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding-bottom: 12px;
+}
+
+.instructions {
+    text-align: center;
+    padding: 1rem;
+}
+
+.dots-row {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    padding-bottom: 1rem;
+}
+
+.dots-holder {
+    display: flex;
+    padding: 0.25rem 0.1rem;
+    border-radius: 1rem;
+    background: $motion-light-transparent;
+}
+
+.dots-holder-success {
+    background: $pen-transparent;
+}
+
+.dots-holder-error {
+    background: $error-transparent;
+}
+
+.dot {
+    width: 0.5rem;
+    height: 0.5rem;
+    margin: 0 0.3rem;
+    border-radius: 100%;
+}
+
+.inactive-step-dot {
+    background: $motion-transparent;
+}
+
+.active-step-dot {
+    background: $motion-primary;
+}
+
+.success-dot {
+    background: $pen-primary;
+}
+
+.error-dot {
+    background: $error-primary;
+}
diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5ddcb33e3b891aa5641cbf63d884688cc83c0183
--- /dev/null
+++ b/src/components/connection-modal/connection-modal.jsx
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import keyMirror from 'keymirror';
+
+import Box from '../box/box.jsx';
+import Modal from '../modal/modal.jsx';
+
+import ScanningStep from '../../containers/scanning-step.jsx';
+import ConnectingStep from './connecting-step.jsx';
+import ConnectedStep from './connected-step.jsx';
+import ErrorStep from './error-step.jsx';
+
+import styles from './connection-modal.css';
+
+const PHASES = keyMirror({
+    scanning: null,
+    connecting: null,
+    connected: null,
+    error: null
+});
+
+const ConnectionModalComponent = props => (
+    <Modal
+        className={styles.modalContent}
+        contentLabel={props.name}
+        headerClassName={styles.header}
+        headerImage={props.smallDeviceImage}
+        onRequestClose={props.onCancel}
+    >
+        <Box className={styles.body}>
+            {props.phase === PHASES.scanning && <ScanningStep {...props} />}
+            {props.phase === PHASES.connecting && <ConnectingStep {...props} />}
+            {props.phase === PHASES.connected && <ConnectedStep {...props} />}
+            {props.phase === PHASES.error && <ErrorStep {...props} />}
+        </Box>
+    </Modal>
+);
+
+ConnectionModalComponent.propTypes = {
+    name: PropTypes.node,
+    onCancel: PropTypes.func.isRequired,
+    phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired,
+    smallDeviceImage: PropTypes.string,
+    title: PropTypes.string.isRequired
+};
+
+export {
+    ConnectionModalComponent as default,
+    PHASES
+};
diff --git a/src/components/connection-modal/device-tile.jsx b/src/components/connection-modal/device-tile.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c806453aa0ff60828462376bf9c01a3a38ae550f
--- /dev/null
+++ b/src/components/connection-modal/device-tile.jsx
@@ -0,0 +1,87 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import Box from '../box/box.jsx';
+
+import styles from './connection-modal.css';
+
+class DeviceTile extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleConnecting'
+        ]);
+    }
+    handleConnecting () {
+        this.props.onConnecting(this.props.peripheralId);
+    }
+    render () {
+        return (
+            <Box className={styles.deviceTile}>
+                <Box className={styles.deviceTileName}>
+                    <img
+                        className={styles.deviceTileImage}
+                        src={this.props.smallDeviceImage}
+                    />
+                    <Box className={styles.deviceTileNameWrapper}>
+                        <Box className={styles.deviceTileNameLabel}>
+                            <FormattedMessage
+                                defaultMessage="Device name"
+                                description="Label for field showing the device name"
+                                id="gui.connection.device-name-label"
+                            />
+                        </Box>
+                        <Box className={styles.deviceTileNameText}>
+                            {this.props.name}
+                        </Box>
+                    </Box>
+                </Box>
+                <Box className={styles.deviceTileWidgets}>
+                    <Box className={styles.signalStrengthMeter}>
+                        <div
+                            className={classNames(styles.signalBar, {
+                                [styles.greenBar]: this.props.rssi > -80
+                            })}
+                        />
+                        <div
+                            className={classNames(styles.signalBar, {
+                                [styles.greenBar]: this.props.rssi > -60
+                            })}
+                        />
+                        <div
+                            className={classNames(styles.signalBar, {
+                                [styles.greenBar]: this.props.rssi > -40
+                            })}
+                        />
+                        <div
+                            className={classNames(styles.signalBar, {
+                                [styles.greenBar]: this.props.rssi > -20
+                            })}
+                        />
+                    </Box>
+                    <button
+                        onClick={this.handleConnecting}
+                    >
+                        <FormattedMessage
+                            defaultMessage="Connect"
+                            description="Button to start connecting to a specific device"
+                            id="gui.connection.connect"
+                        />
+                    </button>
+                </Box>
+            </Box>
+        );
+    }
+}
+
+DeviceTile.propTypes = {
+    name: PropTypes.string,
+    onConnecting: PropTypes.func,
+    peripheralId: PropTypes.string,
+    rssi: PropTypes.number,
+    smallDeviceImage: PropTypes.string
+};
+
+export default DeviceTile;
diff --git a/src/components/connection-modal/dots.jsx b/src/components/connection-modal/dots.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..23814eaf75ecffed01d5f964da940e8c22277448
--- /dev/null
+++ b/src/components/connection-modal/dots.jsx
@@ -0,0 +1,59 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import classNames from 'classnames';
+
+import Box from '../box/box.jsx';
+import styles from './connection-modal.css';
+
+const Dots = props => (
+    <Box className={styles.dotsRow}>
+        <div
+            className={classNames(
+                styles.dotsHolder,
+                {
+                    [styles.dotsHolderError]: props.error,
+                    [styles.dotsHolderSuccess]: props.success
+                }
+            )}
+        >
+            {Array(props.total).fill(0)
+                .map((_, i) => {
+                    let type = 'inactive';
+                    if (props.counter === i) type = 'active';
+                    if (props.success) type = 'success';
+                    if (props.error) type = 'error';
+                    return (<Dot
+                        key={`dot-${i}`}
+                        type={type}
+                    />);
+                })}
+        </div>
+    </Box>
+);
+
+Dots.propTypes = {
+    counter: PropTypes.number,
+    error: PropTypes.bool,
+    success: PropTypes.bool,
+    total: PropTypes.number
+};
+
+const Dot = props => (
+    <div
+        className={classNames(
+            styles.dot,
+            {
+                [styles.inactiveStepDot]: props.type === 'inactive',
+                [styles.activeStepDot]: props.type === 'active',
+                [styles.successDot]: props.type === 'success',
+                [styles.errorDot]: props.type === 'error'
+            }
+        )}
+    />
+);
+
+Dot.propTypes = {
+    type: PropTypes.string
+};
+
+export default Dots;
diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..681d74b72a2b278b13ae23d39d853becf5b4116d
--- /dev/null
+++ b/src/components/connection-modal/error-step.jsx
@@ -0,0 +1,76 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Box from '../box/box.jsx';
+import Dots from './dots.jsx';
+import helpIcon from './icons/help.svg';
+import backIcon from './icons/back.svg';
+
+import styles from './connection-modal.css';
+
+const ErrorStep = props => (
+    <Box className={styles.body}>
+        <Box className={styles.activityArea}>
+            <Box className={styles.centeredRow}>
+                <div className={styles.deviceActivity}>
+                    <img
+                        className={styles.deviceActivityIcon}
+                        src={props.deviceImage}
+                    />
+                </div>
+            </Box>
+        </Box>
+        <Box className={styles.bottomArea}>
+            <div className={styles.instructions}>
+                <FormattedMessage
+                    defaultMessage="Oops, looks like something went wrong."
+                    description="The device connection process has encountered an error."
+                    id="gui.connection.errorMessage"
+                />
+            </div>
+            <Dots
+                error
+                total={3}
+            />
+            <Box className={styles.buttonRow}>
+                <button
+                    className={styles.connectionButton}
+                    onClick={props.onScanning}
+                >
+                    <img
+                        className={styles.buttonIconLeft}
+                        src={backIcon}
+                    />
+                    <FormattedMessage
+                        defaultMessage="Try again"
+                        description="Button to initiate trying the device connection again after an error"
+                        id="gui.connection.tryagainbutton"
+                    />
+                </button>
+                <button
+                    className={styles.connectionButton}
+                    onClick={props.onHelp}
+                >
+                    <img
+                        className={styles.buttonIconLeft}
+                        src={helpIcon}
+                    />
+                    <FormattedMessage
+                        defaultMessage="Help"
+                        description="Button to view help content"
+                        id="gui.connection.helpbutton"
+                    />
+                </button>
+            </Box>
+        </Box>
+    </Box>
+);
+
+ErrorStep.propTypes = {
+    deviceImage: PropTypes.string.isRequired,
+    onHelp: PropTypes.func,
+    onScanning: PropTypes.func
+};
+
+export default ErrorStep;
diff --git a/src/components/connection-modal/icons/back.svg b/src/components/connection-modal/icons/back.svg
new file mode 100644
index 0000000000000000000000000000000000000000..42f7508f279eaeee36aac930e3f343d2bfc7c518
Binary files /dev/null and b/src/components/connection-modal/icons/back.svg differ
diff --git a/src/components/connection-modal/icons/bluetooth-white.svg b/src/components/connection-modal/icons/bluetooth-white.svg
new file mode 100644
index 0000000000000000000000000000000000000000..df2ae141ebce92e1af1ad8f3f6e90b57c8248225
Binary files /dev/null and b/src/components/connection-modal/icons/bluetooth-white.svg differ
diff --git a/src/components/connection-modal/icons/cancel.svg b/src/components/connection-modal/icons/cancel.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b30decdb65a22a832208426f4258a5fa1dea00b7
Binary files /dev/null and b/src/components/connection-modal/icons/cancel.svg differ
diff --git a/src/components/connection-modal/icons/close.svg b/src/components/connection-modal/icons/close.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a537fc800d01115552a00073103be09cf5ce9fcf
Binary files /dev/null and b/src/components/connection-modal/icons/close.svg differ
diff --git a/src/components/connection-modal/icons/help.svg b/src/components/connection-modal/icons/help.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2938e8340ebb54dd699f80eaf48794bd383a96a4
Binary files /dev/null and b/src/components/connection-modal/icons/help.svg differ
diff --git a/src/components/connection-modal/icons/refresh.svg b/src/components/connection-modal/icons/refresh.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3d4aebb47071a08f9214c5e4c5eafb035e2b86ea
Binary files /dev/null and b/src/components/connection-modal/icons/refresh.svg differ
diff --git a/src/components/connection-modal/icons/searching.png b/src/components/connection-modal/icons/searching.png
new file mode 100644
index 0000000000000000000000000000000000000000..260f3227f2ef09cc1aa6fbca0fc928e2b5be0e28
Binary files /dev/null and b/src/components/connection-modal/icons/searching.png differ
diff --git a/src/components/connection-modal/scanning-step.jsx b/src/components/connection-modal/scanning-step.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1728e1fd5d55df75790f0301ef00386783d95b15
--- /dev/null
+++ b/src/components/connection-modal/scanning-step.jsx
@@ -0,0 +1,103 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Box from '../box/box.jsx';
+import DeviceTile from './device-tile.jsx';
+import Dots from './dots.jsx';
+
+import radarIcon from './icons/searching.png';
+import refreshIcon from './icons/refresh.svg';
+
+import styles from './connection-modal.css';
+
+const ScanningStep = props => (
+    <Box className={styles.body}>
+        <Box className={styles.activityArea}>
+            {props.scanning ? (
+                props.deviceList.length === 0 ? (
+                    <div className={styles.activityAreaInfo}>
+                        <div className={styles.centeredRow}>
+                            <img
+                                className={styles.radar}
+                                src={radarIcon}
+                            />
+                            <FormattedMessage
+                                defaultMessage="Looking for devices"
+                                description="Text shown while scanning for devices"
+                                id="gui.connection.scanning.lookingfordevices"
+                            />
+                        </div>
+                    </div>
+                ) : (
+                    <Box className={styles.deviceTilePane}>
+                        {props.deviceList.map(device =>
+                            (<DeviceTile
+                                key={device.peripheralId}
+                                name={device.name}
+                                peripheralId={device.peripheralId}
+                                rssi={device.rssi}
+                                smallDeviceImage={props.smallDeviceImage}
+                                onConnecting={props.onConnecting}
+                            />)
+                        )}
+                    </Box>
+                )
+            ) : (
+                <Box className={styles.instructions}>
+                    <FormattedMessage
+                        defaultMessage="No devices found"
+                        description="Text shown when no devices could be found"
+                        id="gui.connection.scanning.noDevicesFound"
+                    />
+                </Box>
+            )}
+        </Box>
+        <Box className={styles.bottomArea}>
+            <Box className={styles.instructions}>
+                <FormattedMessage
+                    defaultMessage="Select your device in the list above."
+                    description="Prompt for choosing a device to connect to"
+                    id="gui.connection.scanning.instructions"
+                />
+            </Box>
+            <Dots
+                counter={0}
+                total={3}
+            />
+            <button
+                className={styles.connectionButton}
+                onClick={props.onRefresh}
+            >
+                <FormattedMessage
+                    defaultMessage="Refresh"
+                    description="Button in prompt for starting a search"
+                    id="gui.connection.search"
+                />
+                <img
+                    className={styles.buttonIconRight}
+                    src={refreshIcon}
+                />
+            </button>
+        </Box>
+    </Box>
+);
+
+ScanningStep.propTypes = {
+    deviceList: PropTypes.arrayOf(PropTypes.shape({
+        name: PropTypes.string,
+        rssi: PropTypes.number,
+        peripheralId: PropTypes.string
+    })),
+    onConnecting: PropTypes.func,
+    onRefresh: PropTypes.func,
+    scanning: PropTypes.bool.isRequired,
+    smallDeviceImage: PropTypes.string
+};
+
+ScanningStep.defaultProps = {
+    deviceList: [],
+    scanning: true
+};
+
+export default ScanningStep;
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
index 3aa21280beba80b61181349e5ae8a1bc38b52459..9a5a035a7de18edba2d6d5343fdc4fa97f213878 100644
--- a/src/components/modal/modal.css
+++ b/src/components/modal/modal.css
@@ -83,6 +83,10 @@ $sides: 20rem;
     user-select: none;
 }
 
+.header-image {
+    margin-right: 0.5rem;
+}
+
 .header-item-filter {
     display: flex;
     flex-basis: $sides;
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
index dff3ce8537b748e0c5c554d25652b8f30615efdc..940af75586f59e05291f9970b5bf972f353ccdb3 100644
--- a/src/components/modal/modal.jsx
+++ b/src/components/modal/modal.jsx
@@ -26,13 +26,19 @@ const ModalComponent = props => (
             direction="column"
             grow={1}
         >
-            <div className={styles.header}>
+            <div className={classNames(styles.header, props.headerClassName)}>
                 <div
                     className={classNames(
                         styles.headerItem,
                         styles.headerItemTitle
                     )}
                 >
+                    {props.headerImage ? (
+                        <img
+                            className={styles.headerImage}
+                            src={props.headerImage}
+                        />
+                    ) : null}
                     {props.contentLabel}
                 </div>
                 <div
@@ -74,6 +80,8 @@ ModalComponent.propTypes = {
         PropTypes.object
     ]).isRequired,
     fullScreen: PropTypes.bool,
+    headerClassName: PropTypes.string,
+    headerImage: PropTypes.string,
     onRequestClose: PropTypes.func
 };
 
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 40ad9234ed774d4bdbe0b3e0cc8ee78a14de89e7..20c6d482ff5c9a927d731d84200192b514fe2269 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -9,8 +9,10 @@ import VM from 'scratch-vm';
 
 import analytics from '../lib/analytics';
 import Prompt from './prompt.jsx';
+import ConnectionModal from './connection-modal.jsx';
 import BlocksComponent from '../components/blocks/blocks.jsx';
 import ExtensionLibrary from './extension-library.jsx';
+import extensionData from '../lib/libraries/extensions/index.jsx';
 import CustomProcedures from './custom-procedures.jsx';
 import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
 import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
@@ -38,6 +40,9 @@ class Blocks extends React.Component {
             'attachVM',
             'detachVM',
             'handleCategorySelected',
+            'handleConnectionModalStart',
+            'handleConnectionModalClose',
+            'handleStatusButtonUpdate',
             'handlePromptStart',
             'handlePromptCallback',
             'handlePromptClose',
@@ -56,9 +61,11 @@ class Blocks extends React.Component {
             'setLocale'
         ]);
         this.ScratchBlocks.prompt = this.handlePromptStart;
+        this.ScratchBlocks.statusButtonCallback = this.handleConnectionModalStart;
         this.state = {
             workspaceMetrics: {},
-            prompt: null
+            prompt: null,
+            connectionModal: null
         };
         this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
         this.toolboxUpdateQueue = [];
@@ -94,6 +101,7 @@ class Blocks extends React.Component {
     shouldComponentUpdate (nextProps, nextState) {
         return (
             this.state.prompt !== nextState.prompt ||
+            this.state.connectionModal !== nextState.connectionModal ||
             this.props.isVisible !== nextProps.isVisible ||
             this.props.toolboxXML !== nextProps.toolboxXML ||
             this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ||
@@ -311,6 +319,11 @@ class Blocks extends React.Component {
         this.handleExtensionAdded(blocksInfo);
     }
     handleCategorySelected (categoryId) {
+        const extension = extensionData.find(ext => ext.extensionId === categoryId);
+        if (extension && extension.launchDeviceConnectionFlow) {
+            this.handleConnectionModalStart(categoryId);
+        }
+
         this.withToolboxUpdates(() => {
             this.workspace.toolbox_.setSelectedCategoryById(categoryId);
         });
@@ -330,6 +343,23 @@ class Blocks extends React.Component {
             p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE;
         this.setState(p);
     }
+    handleConnectionModalStart (extensionId) {
+        const extension = extensionData.find(ext => ext.extensionId === extensionId);
+        if (extension) {
+            this.setState({connectionModal: {
+                extensionId: extensionId,
+                deviceImage: extension.deviceImage,
+                smallDeviceImage: extension.smallDeviceImage,
+                name: extension.name
+            }});
+        }
+    }
+    handleConnectionModalClose () {
+        this.setState({connectionModal: null});
+    }
+    handleStatusButtonUpdate (extensionId, status) {
+        this.ScratchBlocks.updateStatusButton(this.workspace, extensionId, status);
+    }
     handlePromptCallback (input, optionSelection) {
         this.state.prompt.callback(input, optionSelection,
             (optionSelection === 'local') ? [] :
@@ -381,6 +411,17 @@ class Blocks extends React.Component {
                         onOk={this.handlePromptCallback}
                     />
                 ) : null}
+                {this.state.connectionModal ? (
+                    <ConnectionModal
+                        deviceImage={this.state.connectionModal.deviceImage}
+                        extensionId={this.state.connectionModal.extensionId}
+                        name={this.state.connectionModal.name}
+                        smallDeviceImage={this.state.connectionModal.smallDeviceImage}
+                        vm={vm}
+                        onCancel={this.handleConnectionModalClose}
+                        onStatusButtonUpdate={this.handleStatusButtonUpdate}
+                    />
+                ) : null}
                 {extensionLibraryVisible ? (
                     <ExtensionLibrary
                         vm={vm}
diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b256b03095835eca16d1d33520a8185fbe6b7461
--- /dev/null
+++ b/src/containers/connection-modal.jsx
@@ -0,0 +1,104 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import ConnectionModalComponent, {PHASES} from '../components/connection-modal/connection-modal.jsx';
+import VM from 'scratch-vm';
+
+class ConnectionModal extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleScanning',
+            'handleConnected',
+            'handleConnecting',
+            'handleDisconnect',
+            'handleError'
+        ]);
+        this.state = {
+            phase: PHASES.scanning
+        };
+    }
+    componentDidMount () {
+        this.props.vm.on('PERIPHERAL_CONNECTED', this.handleConnected);
+        this.props.vm.on('PERIPHERAL_ERROR', this.handleError);
+
+        // Check if we're already connected
+        if (this.props.vm.getPeripheralIsConnected(this.props.extensionId)) {
+            this.handleConnected();
+        }
+
+    }
+    componentWillUnmount () {
+        this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleConnected);
+        this.props.vm.removeListener('PERIPHERAL_ERROR', this.handleError);
+    }
+    handleScanning () {
+        this.setState({
+            phase: PHASES.scanning
+        });
+    }
+    handleConnecting (peripheralId) {
+        this.props.vm.connectToPeripheral(this.props.extensionId, peripheralId);
+        this.setState({
+            phase: PHASES.connecting
+        });
+    }
+    handleDisconnect () {
+        this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready');
+        this.props.vm.disconnectExtensionSession(this.props.extensionId);
+        this.props.onCancel();
+    }
+    handleCancel () {
+        // If we're not connected to a device, close the websocket so we stop scanning.
+        if (!this.props.vm.getPeripheralIsConnected(this.props.extensionId)) {
+            this.props.vm.disconnectExtensionSession(this.props.extensionId);
+        }
+        this.props.onCancel();
+    }
+    handleError () {
+        this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready');
+        this.setState({
+            phase: PHASES.error
+        });
+    }
+    handleConnected () {
+        this.props.onStatusButtonUpdate(this.props.extensionId, 'ready');
+        this.setState({
+            phase: PHASES.connected
+        });
+    }
+    handleHelp () {
+        // @todo: implement the help button
+    }
+    render () {
+        return (
+            <ConnectionModalComponent
+                deviceImage={this.props.deviceImage}
+                extensionId={this.props.extensionId}
+                name={this.props.name}
+                phase={this.state.phase}
+                smallDeviceImage={this.props.smallDeviceImage}
+                title={this.props.extensionId}
+                vm={this.props.vm}
+                onCancel={this.props.onCancel}
+                onConnected={this.handleConnected}
+                onConnecting={this.handleConnecting}
+                onDisconnect={this.handleDisconnect}
+                onHelp={this.handleHelp}
+                onScanning={this.handleScanning}
+            />
+        );
+    }
+}
+
+ConnectionModal.propTypes = {
+    deviceImage: PropTypes.string.isRequired,
+    extensionId: PropTypes.string.isRequired,
+    name: PropTypes.node.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onStatusButtonUpdate: PropTypes.func.isRequired,
+    smallDeviceImage: PropTypes.string.isRequired,
+    vm: PropTypes.instanceOf(VM).isRequired
+};
+
+export default ConnectionModal;
diff --git a/src/containers/scanning-step.jsx b/src/containers/scanning-step.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ecc19318913845154c248f716dfad6d440147c2e
--- /dev/null
+++ b/src/containers/scanning-step.jsx
@@ -0,0 +1,74 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import ScanningStepComponent from '../components/connection-modal/scanning-step.jsx';
+import VM from 'scratch-vm';
+
+class ScanningStep extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handlePeripheralListUpdate',
+            'handlePeripheralScanTimeout',
+            'handleRefresh'
+        ]);
+        this.state = {
+            scanning: true,
+            deviceList: []
+        };
+    }
+    componentDidMount () {
+        this.props.vm.startDeviceScan(this.props.extensionId);
+        this.props.vm.on(
+            'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
+        this.props.vm.on(
+            'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
+    }
+    componentWillUnmount () {
+        // @todo: stop the device scan here
+        this.props.vm.removeListener(
+            'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
+        this.props.vm.removeListener(
+            'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
+    }
+    handlePeripheralScanTimeout () {
+        this.setState({scanning: false});
+    }
+    handlePeripheralListUpdate (newList) {
+        // TODO: sort peripherals by signal strength? so they don't jump around
+        const peripheralArray = Object.keys(newList).map(id =>
+            newList[id]
+        );
+        this.setState({deviceList: peripheralArray});
+    }
+    handleRefresh () {
+        this.props.vm.startDeviceScan(this.props.extensionId);
+        this.setState({
+            scanning: true,
+            deviceList: []
+        });
+    }
+    render () {
+        return (
+            <ScanningStepComponent
+                deviceList={this.state.deviceList}
+                phase={this.state.phase}
+                smallDeviceImage={this.props.smallDeviceImage}
+                title={this.props.extensionId}
+                onConnected={this.props.onConnected}
+                onConnecting={this.props.onConnecting}
+                onRefresh={this.handleRefresh}
+            />
+        );
+    }
+}
+
+ScanningStep.propTypes = {
+    extensionId: PropTypes.string.isRequired,
+    onConnected: PropTypes.func.isRequired,
+    onConnecting: PropTypes.func.isRequired,
+    smallDeviceImage: PropTypes.string,
+    vm: PropTypes.instanceOf(VM).isRequired
+};
+
+export default ScanningStep;
diff --git a/src/css/colors.css b/src/css/colors.css
index 39503771d31f4c543d8ff98bcb2d387155159bd4..37ffa62227a6f6ccb42a6170d4652db037f4f769 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -15,6 +15,7 @@ $text-primary-transparent: hsla(225, 15%, 40%, 0.75);
 $motion-primary: hsla(215, 100%, 65%, 1); /* #4C97FF */
 $motion-tertiary: hsla(215, 60%, 50%, 1); /* #3373CC */
 $motion-transparent: hsla(215, 100%, 65%, 0.35); /* 35% transparent version of motion-primary */
+$motion-light-transparent: hsla(215, 100%, 65%, 0.1); /* 10% transparent version of motion-primary */
 
 $red-primary: hsla(20, 100%, 55%, 1); /* #FF661A */
 $red-tertiary: hsla(20, 100%, 45%, 1); /* #E64D00 */
@@ -26,6 +27,12 @@ $control-primary: hsla(38, 100%, 55%, 1); /* #FFAB19 */
 
 $data-primary: hsla(30, 100%, 55%, 1); /* #FF8C1A */
 
+$pen-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */
+$pen-transparent: hsla(163, 85%, 40%, 0.25); /* #0FBD8C */
+
+$error-primary: hsla(30, 100%, 55%, 1); /* #FF8C1A */
+$error-transparent: hsla(30, 100%, 55%, 0.25); /* #FF8C1A */
+
 $extensions-primary: hsla(163, 85%, 40%, 1); /* #0FBD8C */
 $extensions-tertiary: hsla(163, 85%, 30%, 1); /* #0B8E69 */
 $extensions-transparent: hsla(163, 85%, 40%, 0.35); /* 35% transparent version of extensions-primary */
diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg b/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7f8c8c395969e202e01d4e18c0225b5ab98c49b0
Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg differ
diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-menu-icon.svg b/src/lib/libraries/extensions/device-connection/ev3/ev3-menu-icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7c8ea12abb768d48c8d544b1984fabf621e28c42
Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/ev3/ev3-menu-icon.svg differ
diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg b/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3ef892bcc456fd9196e72356742391fd74b67a93
Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg differ
diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-menu-icon.svg b/src/lib/libraries/extensions/device-connection/microbit/microbit-menu-icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..af97b7948e8930b49c230a7386517aef091e0a51
Binary files /dev/null and b/src/lib/libraries/extensions/device-connection/microbit/microbit-menu-icon.svg differ
diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx
index 71404f8fb0e2abd8a47018569487c350ad581074..71b1f2fe9ef34d6247411fcf62dad2d97fbd7c93 100644
--- a/src/lib/libraries/extensions/index.jsx
+++ b/src/lib/libraries/extensions/index.jsx
@@ -11,6 +11,12 @@ import ev3Image from './ev3.png';
 import boostImage from './boost.png';
 import translateImage from './translate.png';
 
+import ev3DeviceImage from './device-connection/ev3/ev3-hub-illustration.svg';
+import ev3MenuImage from './device-connection/ev3/ev3-menu-icon.svg';
+
+import microbitDeviceImage from './device-connection/microbit/microbit-illustration.svg';
+import microbitMenuImage from './device-connection/microbit/microbit-menu-icon.svg';
+
 export default [
     {
         name: (
@@ -120,7 +126,10 @@ export default [
             />
         ),
         featured: true,
-        disabled: true
+        disabled: true,
+        launchDeviceConnectionFlow: true,
+        deviceImage: microbitDeviceImage,
+        smallDeviceImage: microbitMenuImage
     },
     {
         name: 'LEGO WeDo 2.0',
@@ -148,7 +157,10 @@ export default [
             />
         ),
         featured: true,
-        disabled: true
+        disabled: true,
+        launchDeviceConnectionFlow: true,
+        deviceImage: ev3DeviceImage,
+        smallDeviceImage: ev3MenuImage
     },
     {
         name: 'LEGO Boost',