diff --git a/res/css/_components.scss b/res/css/_components.scss
index 582dc59517..fa388c4e6a 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -86,6 +86,7 @@
 @import "./views/elements/_Field.scss";
 @import "./views/elements/_ImageView.scss";
 @import "./views/elements/_InlineSpinner.scss";
+@import "./views/elements/_InteractiveTooltip.scss";
 @import "./views/elements/_ManageIntegsButton.scss";
 @import "./views/elements/_MemberEventListSummary.scss";
 @import "./views/elements/_MessageEditor.scss";
diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss
new file mode 100644
index 0000000000..a3f5b6edc2
--- /dev/null
+++ b/res/css/views/elements/_InteractiveTooltip.scss
@@ -0,0 +1,101 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_InteractiveTooltip_wrapper {
+    position: fixed;
+    z-index: 5000;
+}
+
+.mx_InteractiveTooltip_background {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    opacity: 1.0;
+    z-index: 5000;
+}
+
+.mx_InteractiveTooltip {
+    border-radius: 3px;
+    background-color: $interactive-tooltip-bg-color;
+    color: $interactive-tooltip-fg-color;
+    position: absolute;
+    font-size: 10px;
+    font-weight: 600;
+    padding: 6px;
+    z-index: 5001;
+}
+
+.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top {
+    top: 10px; // 8px chevron + 2px spacing
+}
+
+.mx_InteractiveTooltip_chevron_top {
+    position: absolute;
+    left: calc(50% - 8px);
+    top: -8px;
+    width: 0;
+    height: 0;
+    border-left: 8px solid transparent;
+    border-bottom: 8px solid $interactive-tooltip-bg-color;
+    border-right: 8px solid transparent;
+}
+
+// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
+// by Sebastiano Guerriero (@guerriero_se)
+@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
+    .mx_InteractiveTooltip_chevron_top {
+        height: 16px;
+        width: 16px;
+        background-color: inherit;
+        border: none;
+        clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
+        transform: rotate(135deg);
+        border-radius: 0 0 0 3px;
+        top: calc(-8px / 1.414); // sqrt(2) because of rotation
+    }
+}
+
+.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom {
+    bottom: 10px; // 8px chevron + 2px spacing
+}
+
+.mx_InteractiveTooltip_chevron_bottom {
+    position: absolute;
+    left: calc(50% - 8px);
+    bottom: -8px;
+    width: 0;
+    height: 0;
+    border-left: 8px solid transparent;
+    border-top: 8px solid $interactive-tooltip-bg-color;
+    border-right: 8px solid transparent;
+}
+
+// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path
+// by Sebastiano Guerriero (@guerriero_se)
+@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) {
+    .mx_InteractiveTooltip_chevron_bottom {
+        height: 16px;
+        width: 16px;
+        background-color: inherit;
+        border: none;
+        clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
+        transform: rotate(-45deg);
+        border-radius: 0 0 0 3px;
+        bottom: calc(-8px / 1.414); // sqrt(2) because of rotation
+    }
+}
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index bdccf71540..ed1cc162a0 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -160,6 +160,9 @@ $reaction-row-button-selected-border-color: $accent-color;
 $tooltip-timeline-bg-color: $tagpanel-bg-color;
 $tooltip-timeline-fg-color: #ffffff;
 
+$interactive-tooltip-bg-color: $base-color;
+$interactive-tooltip-fg-color: #ffffff;
+
 // ***** Mixins! *****
 
 @define-mixin mx_DialogButton {
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 8244485ee3..361f6fa408 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -272,6 +272,9 @@ $reaction-row-button-selected-border-color: $accent-color;
 $tooltip-timeline-bg-color: $tagpanel-bg-color;
 $tooltip-timeline-fg-color: #ffffff;
 
+$interactive-tooltip-bg-color: #27303a;
+$interactive-tooltip-fg-color: #ffffff;
+
 // ***** Mixins! *****
 
 @define-mixin mx_DialogButton {
diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js
new file mode 100644
index 0000000000..b3d0b32fa7
--- /dev/null
+++ b/src/components/views/elements/InteractiveTooltip.js
@@ -0,0 +1,145 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container";
+
+function getOrCreateContainer() {
+    let container = document.getElementById(InteractiveTooltipContainerId);
+
+    if (!container) {
+        container = document.createElement("div");
+        container.id = InteractiveTooltipContainerId;
+        document.body.appendChild(container);
+    }
+
+    return container;
+}
+
+/*
+ * This style of tooltip takes a `target` element's rect and centers the tooltip
+ * along one edge of the target.
+ */
+export default class InteractiveTooltip extends React.Component {
+    propTypes: {
+        // A DOMRect from the target element
+        targetRect: PropTypes.object.isRequired,
+        // Function to be called on menu close
+        onFinished: PropTypes.func,
+        // If true, insert an invisible screen-sized element behind the
+        // menu that when clicked will close it.
+        hasBackground: PropTypes.bool,
+        // The component to render as the context menu
+        elementClass: PropTypes.element.isRequired,
+        // on resize callback
+        windowResize: PropTypes.func,
+        // method to close menu
+        closeTooltip: PropTypes.func,
+    };
+
+    constructor() {
+        super();
+
+        this.state = {
+            contentRect: null,
+        };
+    }
+
+    collectContentRect = (element) => {
+        // We don't need to clean up when unmounting, so ignore
+        if (!element) return;
+
+        this.setState({
+            contentRect: element.getBoundingClientRect(),
+        });
+    }
+
+    render() {
+        const props = this.props;
+        const { targetRect } = props;
+
+        // The window X and Y offsets are to adjust position when zoomed in to page
+        const targetLeft = targetRect.left + window.pageXOffset;
+        const targetBottom = targetRect.bottom + window.pageYOffset;
+        const targetTop = targetRect.top + window.pageYOffset;
+
+        // Align the tooltip vertically on whichever side of the target has more
+        // space available.
+        const position = {};
+        let chevronFace = null;
+        if (targetBottom < window.innerHeight / 2) {
+            position.top = targetBottom;
+            chevronFace = "top";
+        } else {
+            position.bottom = window.innerHeight - targetTop;
+            chevronFace = "bottom";
+        }
+
+        // Center the tooltip horizontally with the target's center.
+        position.left = targetLeft + targetRect.width / 2;
+
+        const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
+
+        const menuClasses = classNames({
+            'mx_InteractiveTooltip': true,
+            'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top',
+            'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom',
+        });
+
+        const menuStyle = {};
+        if (this.state.contentRect) {
+            menuStyle.left = `-${this.state.contentRect.width / 2}px`;
+        }
+
+        const ElementClass = props.elementClass;
+
+        return <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
+            <div className={menuClasses} style={menuStyle} ref={this.collectContentRect}>
+                { chevron }
+                <ElementClass {...props} onFinished={props.closeTooltip} onResize={props.windowResize} />
+            </div>
+            { props.hasBackground && <div className="mx_InteractiveTooltip_background"
+                                          onClick={props.closeTooltip} /> }
+        </div>;
+    }
+}
+
+export function createTooltip(ElementClass, props, hasBackground=true) {
+    const closeTooltip = function(...args) {
+        ReactDOM.unmountComponentAtNode(getOrCreateContainer());
+
+        if (props && props.onFinished) {
+            props.onFinished.apply(null, args);
+        }
+    };
+
+    // We only reference closeTooltip once per call to createTooltip
+    const menu = <InteractiveTooltip
+        hasBackground={hasBackground}
+        {...props}
+        elementClass={ElementClass}
+        closeTooltip={closeTooltip} // eslint-disable-line react/jsx-no-bind
+        windowResize={closeTooltip} // eslint-disable-line react/jsx-no-bind
+    />;
+
+    ReactDOM.render(menu, getOrCreateContainer());
+
+    return {close: closeTooltip};
+}