diff --git a/css/_atlaskit_overrides.scss b/css/_atlaskit_overrides.scss
index 519ffaa69..1261dc772 100644
--- a/css/_atlaskit_overrides.scss
+++ b/css/_atlaskit_overrides.scss
@@ -100,12 +100,18 @@
 }
 
 .audio-preview > div:nth-child(2),
-.video-preview > div:nth-child(2) {
+.video-preview > div:nth-child(2),
+.reactions-menu-popup > div:nth-child(2) {
     margin-bottom: 4px;
     outline: none;
     padding: 0;
 }
 
+.reactions-menu-popup > div:nth-child(2) {
+    margin-bottom: 6px;
+    box-shadow: none;
+}
+
 /**
  * The following selectors keep the chat modal full-size anywhere between 100px
  * and 580px for desktop or 680px for mobile.
diff --git a/css/_drawer.scss b/css/_drawer.scss
index 943855fc9..9e3cd319d 100644
--- a/css/_drawer.scss
+++ b/css/_drawer.scss
@@ -4,17 +4,16 @@
     right: 0;
     bottom: 0;
     z-index: $drawerZ;
+    background-color: #141414;
+    border-radius: 16px 16px 0 0;
 }
 
 .drawer-menu {
-    max-height: 50vh;
+    max-height: calc(80vh - 64px);
     background: #242528;
     border-radius: 16px 16px 0 0;
-    overflow-y: auto;
-
-    &.expanded {
-        max-height: 80vh;
-    }
+    overflow-y: hidden;
+    margin-bottom: env(safe-area-inset-bottom, 0);
 
     .drawer-toggle {
         display: flex;
@@ -42,6 +41,8 @@
         font-size: 1.2em;
         list-style-type: none;
         padding: 0;
+        height: calc(80vh - 144px - 64px);
+        overflow-y: auto;
 
         .overflow-menu-item {
             box-sizing: border-box;
diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss
new file mode 100644
index 000000000..2301db282
--- /dev/null
+++ b/css/_reactions-menu.scss
@@ -0,0 +1,189 @@
+@use 'sass:math';
+
+.reactions-menu {
+	width: 280px;
+	background: #292929;
+	box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
+	border-radius: 3px;
+	padding: 16px;
+
+	&.overflow {
+		width: auto;
+		padding-bottom: max(env(safe-area-inset-bottom, 0), 16px);
+		background-color: #141414;
+		box-shadow: none;
+		border-radius: 0;
+		position: relative;
+
+		.toolbox-icon {
+			width: 48px;
+			height: 48px;
+
+			span.emoji {
+				width: 48px;
+				height: 48px;
+			}
+		}
+
+		.reactions-row {
+			display: flex;
+			flex-direction: row;
+			justify-content: space-around;
+
+			.toolbox-button {
+				margin-right: 0;
+			}
+		}
+	}
+
+	.toolbox-icon {
+		width: 40px;
+		height: 40px;
+		border-radius: 6px;
+
+		span.emoji {
+			width: 40px;
+			height: 40px;
+			font-size: 22px;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+		}
+	}
+
+	.reactions-row {
+		.toolbox-button {
+			margin-right: 8px;
+			touch-action: manipulation;
+		}
+
+		.toolbox-button:last-of-type {
+			margin-right: 0;
+		}
+	}
+
+	.raise-hand-row {
+		margin-top: 16px;
+
+		.toolbox-button {
+			width: 100%;
+		}
+
+		.toolbox-icon {
+			width: 100%;
+			flex-direction: row;
+			align-items: center;
+
+			span.text {
+				font-style: normal;
+				font-weight: 600;
+				font-size: 14px;
+				line-height: 24px;
+				margin-left: 8px;
+			}
+		}
+	}
+}
+
+.reactions-animations-container {
+	position: absolute;
+	width: 20%;
+	bottom: 0;
+	left: 40%;
+	height: 48px;
+}
+
+.reactions-menu-popup-container,
+.reactions-menu-popup {
+	display: inline-block;
+	position: relative;
+}
+
+$reactionCount: 20;
+
+@function random($min, $max) {
+  @return math.random() * ($max - $min) + $min;
+}
+
+.reaction-emoji {
+	position: absolute;
+	font-size: 24px;
+	line-height: 32px;
+	width: 32px;
+	height: 32px;
+	top: 32px;
+	left: 10px;
+	opacity: 0;
+	z-index: 1;
+
+	&.reaction-0 {
+		animation: flowToRight 5s forwards ease-in-out;
+	}
+
+	@for $i from 1 through $reactionCount {
+	&.reaction-#{$i} {
+		animation: animation-#{$i} 5s forwards ease-in-out;
+		top: #{random(50, 0)}px;
+		left: #{random(-10, 10)}px;
+	}
+}
+}
+
+@keyframes flowToRight {
+	0% {
+		transform: translate(0px, 0px) scale(0.6);
+		opacity: 1;
+	}
+
+	70% {
+		transform: translate(40px, -70vh) scale(1.5);
+		opacity: 1;
+	}
+
+	75% {
+		transform: translate(40px, -70vh) scale(1.5);
+		opacity: 1;
+	}
+
+	100% {
+		transform: translate(140px, -50vh) scale(1);
+		opacity: 0;
+	}
+}
+
+@mixin animation-list {
+	@for $i from 1 through $reactionCount {
+		$topX: random(-100, 100);
+		$topY: random(65, 75);
+		$bottomX: random(150, 200);
+		$bottomY: random(40, 50);
+
+		@if $topX < 0 {
+			$bottomX: -$bottomX;
+		}
+
+		@keyframes animation-#{$i} {
+			0% {
+				transform: translate(0, 0) scale(0.6);
+				opacity: 1;
+			}
+
+			70% {
+				transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
+				opacity: 1;
+			}
+
+			75% {
+				transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
+				opacity: 1;
+			}
+
+			100% {
+				transform: translate(#{$bottomX}px, -#{$bottomY}vh) scale(1);
+				opacity: 0;
+			}
+		}
+	}
+}
+
+@include animation-list;
diff --git a/css/_toolbars.scss b/css/_toolbars.scss
index 1d93b7267..491ccd48a 100644
--- a/css/_toolbars.scss
+++ b/css/_toolbars.scss
@@ -105,11 +105,14 @@
     margin: 0 auto;
     max-width: 100%;
     pointer-events: all;
+    background-color: #131519;
+    padding-bottom: env(safe-area-inset-bottom, 0);
+    box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
+    border-radius: 6px;
 }
 
 .toolbox-content-items {
     background: $newToolbarBackgroundColor;
-    box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
     border-radius: 6px;
     margin: 0 auto;
     padding: 6px;
diff --git a/css/filmstrip/_vertical_filmstrip.scss b/css/filmstrip/_vertical_filmstrip.scss
index ec816f066..a9fe54286 100644
--- a/css/filmstrip/_vertical_filmstrip.scss
+++ b/css/filmstrip/_vertical_filmstrip.scss
@@ -28,7 +28,7 @@
     flex-direction: column-reverse;
     height: 100%;
     width: 100%;
-    padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
+    padding: ($desktopAppDragBarHeight - 5px) 5px calc(env(safe-area-inset-bottom, 0) + 10px);
     /**
      * fixed positioning is necessary for remote menus and tooltips to pop
      * out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
diff --git a/css/main.scss b/css/main.scss
index dac44f585..1711755ce 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -106,6 +106,7 @@ $flagsImagePath: "../images/";
 @import 'connection-status';
 @import 'drawer';
 @import 'participants-pane';
+@import 'reactions-menu';
 @import 'plan-limit';
 
 /* Modules END */
diff --git a/lang/main.json b/lang/main.json
index b583a45dd..9ed0574ec 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -808,6 +808,7 @@
             "callQuality": "Manage video quality",
             "cc": "Toggle subtitles",
             "chat": "Open / Close chat",
+            "clap": "Clap",
             "document": "Toggle shared document",
             "download": "Download our apps",
             "embedMeeting": "Embed meeting",
@@ -817,7 +818,9 @@
             "hangup": "Leave the meeting",
             "help": "Help",
             "invite": "Invite people",
+            "joy": "Laughing Crying",
             "kick": "Kick participant",
+            "like": "Thumbs Up",
             "lobbyButton": "Enable/disable lobby mode",
             "localRecording": "Toggle local recording controls",
             "lockRoom": "Toggle meeting password",
@@ -830,10 +833,12 @@
             "muteEveryonesVideo": "Disable everyone's camera",
             "muteEveryoneElsesVideo": "Disable everyone else's camera",
             "participants": "Participants",
+            "party": "Party Popper",
             "pip": "Toggle Picture-in-Picture mode",
             "privateMessage": "Send private message",
             "profile": "Edit your profile",
             "raiseHand": "Raise / Lower your hand",
+            "reactionsMenu": "Open / Close reactions menu",
             "recording": "Toggle recording",
             "remoteMute": "Mute participant",
             "remoteVideoMute": "Disable camera of participant",
@@ -845,7 +850,9 @@
             "shareYourScreen": "Start / Stop sharing your screen",
             "shortcuts": "Toggle shortcuts",
             "show": "Show on stage",
+            "smile": "Smile",
             "speakerStats": "Toggle speaker statistics",
+            "surprised": "Surprised",
             "tileView": "Toggle tile view",
             "toggleCamera": "Toggle camera",
             "toggleFilmstrip": "Toggle filmstrip",
@@ -864,7 +871,9 @@
         "authenticate": "Authenticate",
         "callQuality": "Manage video quality",
         "chat": "Open / Close chat",
+        "clap": "Clap",
         "closeChat": "Close chat",
+        "closeReactionsMenu": "Close reactions menu",
         "documentClose": "Close shared document",
         "documentOpen": "Open shared document",
         "download": "Download our apps",
@@ -878,6 +887,8 @@
         "hangup": "Leave the meeting",
         "help": "Help",
         "invite": "Invite people",
+        "joy": "Joy",
+        "like": "Thumbs Up",
         "lobbyButtonDisable": "Disable lobby mode",
         "lobbyButtonEnable": "Enable lobby mode",
         "login": "Login",
@@ -896,18 +907,27 @@
         "noisyAudioInputTitle": "Your microphone appears to be noisy!",
         "noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
         "openChat": "Open chat",
+        "openReactionsMenu": "Open reactions menu",
         "participants": "Participants",
+        "party": "Celebration",
         "pip": "Enter Picture-in-Picture mode",
         "privateMessage": "Send private message",
         "profile": "Edit your profile",
         "raiseHand": "Raise / Lower your hand",
         "raiseYourHand": "Raise your hand",
+        "reactionClap": "Send clap reaction",
+        "reactionJoy": "Send joy reaction",
+        "reactionLike": "Send thumbs up reaction",
+        "reactionParty": "Send party popper reaction",
+        "reactionSmile": "Send smile reaction",
+        "reactionSurprised": "Send surprised reaction",
         "security": "Security options",
         "Settings": "Settings",
         "shareaudio": "Share audio",
         "sharedvideo": "Share video",
         "shareRoom": "Invite someone",
         "shortcuts": "View shortcuts",
+        "smile": "Smile",
         "speakerStats": "Speaker stats",
         "startScreenSharing": "Start screen sharing",
         "startSubtitles": "Start subtitles",
@@ -915,6 +935,7 @@
         "stopScreenSharing": "Stop screen sharing",
         "stopSubtitles": "Stop subtitles",
         "stopSharedVideo": "Stop video",
+        "surprised": "Surprised",
         "talkWhileMutedPopup": "Trying to speak? You are muted.",
         "tileViewToggle": "Toggle tile view",
         "toggleCamera": "Toggle camera",
diff --git a/modules/API/constants.js b/modules/API/constants.js
index 4d5213719..5079dd190 100644
--- a/modules/API/constants.js
+++ b/modules/API/constants.js
@@ -15,3 +15,8 @@ export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id
  * The payload name for the datachannel/endpoint text message event
  */
 export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
+
+/**
+ * The payload name for the datachannel/endpoint reaction event
+ */
+export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';
diff --git a/modules/keyboardshortcut/keyboardshortcut.js b/modules/keyboardshortcut/keyboardshortcut.js
index e626cfdef..b6cc06276 100644
--- a/modules/keyboardshortcut/keyboardshortcut.js
+++ b/modules/keyboardshortcut/keyboardshortcut.js
@@ -142,20 +142,23 @@ const KeyboardShortcut = {
      * @param exec the function to be executed when the shortcut is pressed
      * @param helpDescription the description of the shortcut that would appear
      * in the help menu
+     * @param altKey whether or not the alt key must be pressed.
      */
     registerShortcut(// eslint-disable-line max-params
             shortcutChar,
             shortcutAttr,
             exec,
-            helpDescription) {
-        _shortcuts.set(shortcutChar, {
+            helpDescription,
+            altKey = false) {
+        _shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
             character: shortcutChar,
             function: exec,
-            shortcutAttr
+            shortcutAttr,
+            altKey
         });
 
         if (helpDescription) {
-            this._addShortcutToHelp(shortcutChar, helpDescription);
+            this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
         }
     },
 
@@ -164,9 +167,10 @@ const KeyboardShortcut = {
      *
      * @param shortcutChar unregisters the given shortcut, which means it will
      * no longer be usable
+     * @param altKey whether or not shortcut is combo with alt key
      */
-    unregisterShortcut(shortcutChar) {
-        _shortcuts.delete(shortcutChar);
+    unregisterShortcut(shortcutChar, altKey = false) {
+        _shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
         _shortcutsHelp.delete(shortcutChar);
     },
 
@@ -175,6 +179,15 @@ const KeyboardShortcut = {
      * @returns {string} e.key or something close if not supported
      */
     _getKeyboardKey(e) {
+        // If alt is pressed a different char can be returned so this takes
+        // the char from the code. It also prefixes with a colon to differentiate
+        // alt combo from simple keypress.
+        if (e.altKey) {
+            const key = e.code.replace('Key', '');
+
+            return `:${key}`;
+        }
+
         // If e.key is a string, then it is assumed it already plainly states
         // the key pressed. This may not be true in all cases, such as with Edge
         // and "?", when the browser cannot properly map a key press event to a
diff --git a/react/features/app/middlewares.any.js b/react/features/app/middlewares.any.js
index e9e941001..1b1f6dc7a 100644
--- a/react/features/app/middlewares.any.js
+++ b/react/features/app/middlewares.any.js
@@ -35,6 +35,7 @@ import '../large-video/middleware';
 import '../lobby/middleware';
 import '../notifications/middleware';
 import '../overlay/middleware';
+import '../reactions/middleware';
 import '../recent-list/middleware';
 import '../recording/middleware';
 import '../rejoin/middleware';
diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js
index 3e06590db..7a6741ff0 100644
--- a/react/features/app/reducers.any.js
+++ b/react/features/app/reducers.any.js
@@ -41,6 +41,7 @@ import '../large-video/reducer';
 import '../lobby/reducer';
 import '../notifications/reducer';
 import '../overlay/reducer';
+import '../reactions/reducer';
 import '../recent-list/reducer';
 import '../recording/reducer';
 import '../settings/reducer';
diff --git a/react/features/base/dialog/components/AbstractDialogContainer.js b/react/features/base/dialog/components/AbstractDialogContainer.js
index edbf102cf..90ca12b99 100644
--- a/react/features/base/dialog/components/AbstractDialogContainer.js
+++ b/react/features/base/dialog/components/AbstractDialogContainer.js
@@ -2,6 +2,8 @@
 
 import React, { Component } from 'react';
 
+import { type ReactionEmojiProps } from '../../../reactions/constants';
+
 /**
  * The type of the React {@code Component} props of {@link DialogContainer}.
  */
@@ -25,7 +27,12 @@ type Props = {
     /**
      * True if the UI is in a compact state where we don't show dialogs.
      */
-    _reducedUI: boolean
+    _reducedUI: boolean,
+
+    /**
+     * Array of reactions to be displayed.
+     */
+    _reactionsQueue: Array<ReactionEmojiProps>
 };
 
 /**
diff --git a/react/features/base/dialog/components/native/BottomSheet.js b/react/features/base/dialog/components/native/BottomSheet.js
index 0f747d1fb..546558ff0 100644
--- a/react/features/base/dialog/components/native/BottomSheet.js
+++ b/react/features/base/dialog/components/native/BottomSheet.js
@@ -49,7 +49,17 @@ type Props = {
     /**
      * Function to render a bottom sheet header element, if necessary.
      */
-    renderHeader: ?Function
+    renderHeader: ?Function,
+
+    /**
+     * Function to render a bottom sheet footer element, if necessary.
+     */
+    renderFooter: ?Function,
+
+    /**
+    * The height of the screen.
+    */
+    _height: number
 };
 
 /**
@@ -80,7 +90,7 @@ class BottomSheet extends PureComponent<Props> {
      * @returns {ReactElement}
      */
     render() {
-        const { _styles, renderHeader } = this.props;
+        const { _styles, renderHeader, renderFooter, _height } = this.props;
 
         return (
             <SlidingView
@@ -99,7 +109,10 @@ class BottomSheet extends PureComponent<Props> {
                     <SafeAreaView
                         style = { [
                             styles.sheetItemContainer,
-                            _styles.sheet
+                            _styles.sheet,
+                            {
+                                maxHeight: _height - 100
+                            }
                         ] }
                         { ...this.panResponder.panHandlers }>
                         <ScrollView
@@ -108,6 +121,7 @@ class BottomSheet extends PureComponent<Props> {
                             style = { styles.scrollView } >
                             { this.props.children }
                         </ScrollView>
+                        { renderFooter && renderFooter() }
                     </SafeAreaView>
                 </View>
             </SlidingView>
@@ -167,7 +181,8 @@ class BottomSheet extends PureComponent<Props> {
  */
 function _mapStateToProps(state) {
     return {
-        _styles: ColorSchemeRegistry.get(state, 'BottomSheet')
+        _styles: ColorSchemeRegistry.get(state, 'BottomSheet'),
+        _height: state['features/base/responsive-ui'].clientHeight
     };
 }
 
diff --git a/react/features/base/dialog/components/native/DialogContainer.js b/react/features/base/dialog/components/native/DialogContainer.js
index d82fce7ee..224504385 100644
--- a/react/features/base/dialog/components/native/DialogContainer.js
+++ b/react/features/base/dialog/components/native/DialogContainer.js
@@ -1,3 +1,7 @@
+import React from 'react';
+
+import { ReactionEmoji } from '../../../../reactions/components';
+import { getReactionsQueue } from '../../../../reactions/functions.any';
 import { connect } from '../../../redux';
 import AbstractDialogContainer, {
     abstractMapStateToProps
@@ -11,6 +15,22 @@ import AbstractDialogContainer, {
  * @extends AbstractDialogContainer
  */
 class DialogContainer extends AbstractDialogContainer {
+
+    /**
+     * Returns the reactions to be displayed.
+     *
+     * @returns {Array<React$Element>}
+     */
+    _renderReactions() {
+        const { _reactionsQueue } = this.props;
+
+        return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
+            index = { index }
+            key = { uid }
+            reaction = { reaction }
+            uid = { uid } />));
+    }
+
     /**
      * Implements React's {@link Component#render()}.
      *
@@ -18,8 +38,18 @@ class DialogContainer extends AbstractDialogContainer {
      * @returns {ReactElement}
      */
     render() {
-        return this._renderDialogContent();
+        return (<React.Fragment>
+            {this._renderReactions()}
+            {this._renderDialogContent()}
+        </React.Fragment>);
     }
 }
 
-export default connect(abstractMapStateToProps)(DialogContainer);
+const mapStateToProps = state => {
+    return {
+        ...abstractMapStateToProps(state),
+        _reactionsQueue: getReactionsQueue(state)
+    };
+};
+
+export default connect(mapStateToProps)(DialogContainer);
diff --git a/react/features/base/dialog/components/native/styles.js b/react/features/base/dialog/components/native/styles.js
index fd119d640..769891f15 100644
--- a/react/features/base/dialog/components/native/styles.js
+++ b/react/features/base/dialog/components/native/styles.js
@@ -33,7 +33,7 @@ export const bottomSheetStyles = {
     },
 
     scrollView: {
-        paddingHorizontal: MD_ITEM_MARGIN_PADDING
+        paddingHorizontal: 0
     },
 
     /**
@@ -117,7 +117,7 @@ const brandedDialogText = {
 };
 
 const brandedDialogLabelStyle = {
-    color: schemeColor('text'),
+    color: ColorPalette.white,
     flexShrink: 1,
     fontSize: MD_FONT_SIZE,
     opacity: 0.90
@@ -130,7 +130,7 @@ const brandedDialogItemContainerStyle = {
 };
 
 const brandedDialogIconStyle = {
-    color: schemeColor('icon'),
+    color: ColorPalette.white,
     fontSize: 24
 };
 
@@ -178,20 +178,24 @@ ColorSchemeRegistry.register('BottomSheet', {
          * Container style for a generic item rendered in the menu.
          */
         style: {
-            ...brandedDialogItemContainerStyle
+            ...brandedDialogItemContainerStyle,
+            backgroundColor: ColorPalette.darkBackground,
+            paddingHorizontal: MD_ITEM_MARGIN_PADDING
         },
 
         /**
          * Additional style that is not directly used as a style object.
          */
-        underlayColor: ColorPalette.overflowMenuItemUnderlay
+        underlayColor: ColorPalette.toggled
     },
 
     /**
      * Bottom sheet's base style.
      */
     sheet: {
-        backgroundColor: schemeColor('background')
+        backgroundColor: ColorPalette.black,
+        borderTopLeftRadius: 16,
+        borderTopRightRadius: 16
     }
 });
 
diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js
index 8dc0295af..1eff622d0 100644
--- a/react/features/chat/middleware.js
+++ b/react/features/chat/middleware.js
@@ -1,5 +1,8 @@
 // @flow
 
+import { batch } from 'react-redux';
+
+import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
 import {
     CONFERENCE_JOINED,
@@ -19,7 +22,18 @@ import {
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
 import { openDisplayNamePrompt } from '../display-name';
+import { ADD_REACTIONS_MESSAGE } from '../reactions/actionTypes';
+import {
+    pushReaction
+} from '../reactions/actions.any';
+import { REACTIONS } from '../reactions/constants';
+import { endpointMessageReceived } from '../subtitles';
 import { showToolbox } from '../toolbox/actions';
+import {
+    hideToolbox,
+    setToolboxTimeout,
+    setToolboxVisible
+} from '../toolbox/actions.web';
 
 import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
 import { addMessage, clearMessages } from './actions';
@@ -143,6 +157,15 @@ MiddlewareRegistry.register(store => next => action => {
         }
         break;
     }
+
+    case ADD_REACTIONS_MESSAGE: {
+        _handleReceivedMessage(store, {
+            id: localParticipant.id,
+            message: action.message,
+            privateMessage: false,
+            timestamp: Date.now()
+        });
+    }
     }
 
     return next(action);
@@ -189,6 +212,7 @@ StateListenerRegistry.register(
  * @returns {void}
  */
 function _addChatMsgListener(conference, store) {
+    const reactions = {};
 
     if (store.getState()['features/base/config'].iAmRecorder) {
         // We don't register anything on web if we are in iAmRecorder mode
@@ -219,6 +243,43 @@ function _addChatMsgListener(conference, store) {
         }
     );
 
+    conference.on(
+        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
+        (...args) => {
+            store.dispatch(endpointMessageReceived(...args));
+
+            if (args && args.length >= 2) {
+                const [ { _id }, eventData ] = args;
+
+                if (eventData.name === ENDPOINT_REACTION_NAME) {
+                    reactions[_id] = reactions[_id] ?? {
+                        timeout: null,
+                        message: ''
+                    };
+                    batch(() => {
+                        store.dispatch(pushReaction(eventData.reaction));
+                        store.dispatch(setToolboxVisible(true));
+                        store.dispatch(setToolboxTimeout(
+                                () => store.dispatch(hideToolbox()),
+                                5000)
+                        );
+                    });
+
+                    clearTimeout(reactions[_id].timeout);
+                    reactions[_id].message = `${reactions[_id].message}${REACTIONS[eventData.reaction].message}`;
+                    reactions[_id].timeout = setTimeout(() => {
+                        _handleReceivedMessage(store, {
+                            id: _id,
+                            message: reactions[_id].message,
+                            privateMessage: false,
+                            timestamp: eventData.timestamp
+                        });
+                        delete reactions[_id];
+                    }, 500);
+                }
+            }
+        });
+
     conference.on(
         JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
             errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
diff --git a/react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js b/react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js
index d54bfc971..670f6947d 100644
--- a/react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js
+++ b/react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js
@@ -67,6 +67,14 @@ class KeyboardShortcutsDialog extends Component<Props> {
      * @returns {ReactElement}
      */
     _renderShortcutsListItem(keyboardKey, translationKey) {
+        let modifierKey = 'Alt';
+
+        if (window.navigator?.platform) {
+            if (window.navigator.platform.indexOf('Mac') !== -1) {
+                modifierKey = '⌥';
+            }
+        }
+
         return (
             <li
                 className = 'shortcuts-list__item'
@@ -78,7 +86,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
                 </span>
                 <span className = 'item-action'>
                     <Lozenge isBold = { true }>
-                        { keyboardKey }
+                        { keyboardKey.startsWith(':')
+                            ? `${modifierKey} + ${keyboardKey.slice(1)}`
+                            : keyboardKey }
                     </Lozenge>
                 </span>
             </li>
diff --git a/react/features/reactions/actionTypes.js b/react/features/reactions/actionTypes.js
new file mode 100644
index 000000000..f3bbf5a75
--- /dev/null
+++ b/react/features/reactions/actionTypes.js
@@ -0,0 +1,55 @@
+/**
+ * The type of the (redux) action which shows/hides the reactions menu.
+ *
+ * {
+ *     type: TOGGLE_REACTIONS_VISIBLE,
+ *     visible: boolean
+ * }
+ */
+export const TOGGLE_REACTIONS_VISIBLE = 'TOGGLE_REACTIONS_VISIBLE';
+
+/**
+ * The type of the action which adds a new reaction to the reactions message and sets
+ * a new timeout.
+ *
+ * {
+ *     type: SET_REACTION_MESSAGE,
+ *     message: string,
+ *     timeoutID: number
+ * }
+ */
+export const SET_REACTIONS_MESSAGE = 'SET_REACTIONS_MESSAGE';
+
+/**
+ * The type of the action which resets the reactions message and timeout.
+ *
+ * {
+ *     type: CLEAR_REACTION_MESSAGE
+ * }
+ */
+export const CLEAR_REACTIONS_MESSAGE = 'CLEAR_REACTIONS_MESSAGE';
+
+/**
+ * The type of the action which sets the reactions queue.
+ *
+ * {
+ *     type: SET_REACTION_QUEUE,
+ *     value: Array
+ * }
+ */
+export const SET_REACTION_QUEUE = 'SET_REACTION_QUEUE';
+
+/**
+ * The type of the action which signals a send reaction to everyone in the conference.
+ */
+export const SEND_REACTION = 'SEND_REACTION';
+
+/**
+ * The type of the action to add a reaction message to the chat.
+ */
+export const ADD_REACTIONS_MESSAGE = 'ADD_REACTIONS_MESSAGE';
+
+/**
+ * The type of action to add a reaction to the queue.
+ */
+export const PUSH_REACTION = 'PUSH_REACTION';
diff --git a/react/features/reactions/actions.any.js b/react/features/reactions/actions.any.js
new file mode 100644
index 000000000..7747525de
--- /dev/null
+++ b/react/features/reactions/actions.any.js
@@ -0,0 +1,108 @@
+// @flow
+
+import {
+    ADD_REACTIONS_MESSAGE,
+    CLEAR_REACTIONS_MESSAGE,
+    PUSH_REACTION,
+    SEND_REACTION,
+    SET_REACTIONS_MESSAGE,
+    SET_REACTION_QUEUE
+} from './actionTypes';
+import { type ReactionEmojiProps } from './constants';
+
+/**
+ * Sets the reaction queue.
+ *
+ * @param {Array} value - The new queue.
+ * @returns {Function}
+ */
+export function setReactionQueue(value: Array<ReactionEmojiProps>) {
+    return {
+        type: SET_REACTION_QUEUE,
+        value
+    };
+}
+
+/**
+ * Appends the reactions message to the chat and resets the state.
+ *
+ * @returns {void}
+ */
+export function flushReactionsToChat() {
+    return {
+        type: CLEAR_REACTIONS_MESSAGE
+    };
+}
+
+/**
+ * Adds a new reaction to the reactions message.
+ *
+ * @param {boolean} value - The new reaction.
+ * @returns {Object}
+ */
+export function addReactionsMessage(value: string) {
+    return {
+        type: SET_REACTIONS_MESSAGE,
+        reaction: value
+    };
+}
+
+/**
+ * Adds a new reaction to the reactions message.
+ *
+ * @param {boolean} value - Reaction to be added to queue.
+ * @returns {Object}
+ */
+export function pushReaction(value: string) {
+    return {
+        type: PUSH_REACTION,
+        reaction: value
+    };
+}
+
+/**
+ * Removes a reaction from the queue.
+ *
+ * @param {number} uid - Id of the reaction to be removed.
+ * @returns {void}
+ */
+export function removeReaction(uid: number) {
+    return (dispatch: Function, getState: Function) => {
+        const queue = getState()['features/reactions'].queue;
+
+        dispatch(setReactionQueue(queue.filter(reaction => reaction.uid !== uid)));
+    };
+}
+
+
+/**
+ * Sends a reaction message to everyone in the conference.
+ *
+ * @param {string} reaction - The reaction to send out.
+ * @returns {{
+ *     type: SEND_REACTION,
+ *     reaction: string
+ * }}
+ */
+export function sendReaction(reaction: string) {
+    return {
+        type: SEND_REACTION,
+        reaction
+    };
+}
+
+/**
+ * Adds a reactions message to the chat.
+ *
+ * @param {string} message - The reactions message to add to chat.
+ * @returns {{
+ *     type: ADD_REACTIONS_MESSAGE,
+ *     message: string
+ * }}
+ */
+export function addReactionsMessageToChat(message: string) {
+    return {
+        type: ADD_REACTIONS_MESSAGE,
+        message
+    };
+}
diff --git a/react/features/reactions/actions.web.js b/react/features/reactions/actions.web.js
new file mode 100644
index 000000000..ffbf5178f
--- /dev/null
+++ b/react/features/reactions/actions.web.js
@@ -0,0 +1,16 @@
+// @flow
+
+import {
+    TOGGLE_REACTIONS_VISIBLE
+} from './actionTypes';
+
+/**
+ * Toggles the visibility of the reactions menu.
+ *
+ * @returns {Function}
+ */
+export function toggleReactionsMenuVisibility() {
+    return {
+        type: TOGGLE_REACTIONS_VISIBLE
+    };
+}
diff --git a/react/features/reactions/components/_.native.js b/react/features/reactions/components/_.native.js
new file mode 100644
index 000000000..738c4d2b8
--- /dev/null
+++ b/react/features/reactions/components/_.native.js
@@ -0,0 +1 @@
+export * from './native';
diff --git a/react/features/reactions/components/_.web.js b/react/features/reactions/components/_.web.js
new file mode 100644
index 000000000..b80c83af3
--- /dev/null
+++ b/react/features/reactions/components/_.web.js
@@ -0,0 +1 @@
+export * from './web';
diff --git a/react/features/reactions/components/index.js b/react/features/reactions/components/index.js
new file mode 100644
index 000000000..cda61441e
--- /dev/null
+++ b/react/features/reactions/components/index.js
@@ -0,0 +1 @@
+export * from './_';
diff --git a/react/features/reactions/components/native/RaiseHandButton.js b/react/features/reactions/components/native/RaiseHandButton.js
new file mode 100644
index 000000000..248d3bfc5
--- /dev/null
+++ b/react/features/reactions/components/native/RaiseHandButton.js
@@ -0,0 +1,165 @@
+// @flow
+
+import React, { Component } from 'react';
+import { Text, TouchableHighlight, View } from 'react-native';
+import { type Dispatch } from 'redux';
+
+import {
+    createToolbarEvent,
+    sendAnalytics
+} from '../../../analytics';
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
+import { translate } from '../../../base/i18n';
+import {
+    getLocalParticipant,
+    raiseHand
+} from '../../../base/participants';
+import { connect } from '../../../base/redux';
+import { type AbstractButtonProps } from '../../../base/toolbox/components';
+
+import { type ReactionStyles } from './ReactionButton';
+
+/**
+ * The type of the React {@code Component} props of {@link RaiseHandButton}.
+ */
+type Props = AbstractButtonProps & {
+
+    /**
+     * The local participant.
+     */
+    _localParticipant: Object,
+
+    /**
+     * Whether the participant raused their hand or not.
+     */
+    _raisedHand: boolean,
+
+    /**
+     * The redux {@code dispatch} function.
+     */
+    dispatch: Dispatch<any>,
+
+    /**
+     * Used for translation
+     */
+    t: Function,
+
+    /**
+     * Used to close the overflow menu after raise hand is clicked.
+     */
+    onCancel: Function,
+
+    /**
+     * Styles for the button.
+     */
+    _styles: ReactionStyles
+};
+
+/**
+ * An implementation of a button to raise or lower hand.
+ */
+class RaiseHandButton extends Component<Props, *> {
+    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
+    label = 'toolbar.raiseYourHand';
+    toggledLabel = 'toolbar.lowerYourHand';
+
+    /**
+     * Initializes a new {@code RaiseHandButton} instance.
+     *
+     * @param {Props} props - The React {@code Component} props to initialize
+     * the new {@code RaiseHandButton} instance with.
+     */
+    constructor(props: Props) {
+        super(props);
+
+        // Bind event handlers so they are only bound once per instance.
+        this._onClick = this._onClick.bind(this);
+        this._toggleRaisedHand = this._toggleRaisedHand.bind(this);
+        this._getLabel = this._getLabel.bind(this);
+    }
+
+    _onClick: () => void;
+
+    _toggleRaisedHand: () => void;
+
+    _getLabel: () => string;
+
+    /**
+     * Handles clicking / pressing the button.
+     *
+     * @returns {void}
+     */
+    _onClick() {
+        this._toggleRaisedHand();
+        this.props.onCancel();
+    }
+
+    /**
+     * Toggles the rased hand status of the local participant.
+     *
+     * @returns {void}
+     */
+    _toggleRaisedHand() {
+        const enable = !this.props._raisedHand;
+
+        sendAnalytics(createToolbarEvent('raise.hand', { enable }));
+
+        this.props.dispatch(raiseHand(enable));
+    }
+
+    /**
+     * Gets the current label, taking the toggled state into account. If no
+     * toggled label is provided, the regular label will also be used in the
+     * toggled state.
+     *
+     * @returns {string}
+     */
+    _getLabel() {
+        const { _raisedHand, t } = this.props;
+
+        return t(_raisedHand ? this.toggledLabel : this.label);
+    }
+
+    /**
+     * Implements React's {@link Component#render()}.
+     *
+     * @inheritdoc
+     * @returns {ReactElement}
+     */
+    render() {
+        const { _styles, t } = this.props;
+
+        return (
+            <TouchableHighlight
+                accessibilityLabel = { t(this.accessibilityLabel) }
+                accessibilityRole = 'button'
+                onPress = { this._onClick }
+                style = { _styles.style }
+                underlayColor = { _styles.underlayColor }>
+                <View style = { _styles.container }>
+                    <Text style = { _styles.emoji }>✋</Text>
+                    <Text style = { _styles.text }>{this._getLabel()}</Text>
+                </View>
+            </TouchableHighlight>
+        );
+    }
+}
+
+/**
+ * Maps part of the Redux state to the props of this component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {Props}
+ */
+function _mapStateToProps(state): Object {
+    const _localParticipant = getLocalParticipant(state);
+
+    return {
+        _localParticipant,
+        _raisedHand: _localParticipant.raisedHand,
+        _styles: ColorSchemeRegistry.get(state, 'Toolbox').raiseHandButton
+    };
+}
+
+export default translate(connect(_mapStateToProps)(RaiseHandButton));
diff --git a/react/features/reactions/components/native/ReactionButton.js b/react/features/reactions/components/native/ReactionButton.js
new file mode 100644
index 000000000..844d1d579
--- /dev/null
+++ b/react/features/reactions/components/native/ReactionButton.js
@@ -0,0 +1,96 @@
+// @flow
+
+import React from 'react';
+import { Text, TouchableHighlight } from 'react-native';
+import { useDispatch } from 'react-redux';
+
+import { translate } from '../../../base/i18n';
+import type { StyleType } from '../../../base/styles';
+import { sendReaction } from '../../actions.any';
+import { REACTIONS } from '../../constants';
+
+
+export type ReactionStyles = {
+
+    /**
+     * Style for the button.
+     */
+    style: StyleType,
+
+    /**
+     * Underlay color for the button.
+     */
+    underlayColor: StyleType,
+
+    /**
+     * Style for the emoji text on the button.
+     */
+    emoji: StyleType,
+
+    /**
+     * Style for the label text on the button.
+     */
+    text?: StyleType,
+
+    /**
+     * Style for text container. Used on raise hand button.
+     */
+    container?: StyleType
+
+}
+
+/**
+ * The type of the React {@code Component} props of {@link ReactionButton}.
+ */
+type Props = {
+
+    /**
+     * Collection of styles for the button.
+     */
+    styles: ReactionStyles,
+
+    /**
+     * The reaction to be sent
+     */
+    reaction: string,
+
+    /**
+     * Invoked to obtain translated strings.
+     */
+    t: Function
+};
+
+/**
+ * An implementation of a button to send a reaction.
+ *
+ * @returns {ReactElement}
+ */
+function ReactionButton({
+    styles,
+    reaction,
+    t
+}: Props) {
+    const dispatch = useDispatch();
+
+    /**
+     * Handles clicking / pressing the button.
+     *
+     * @returns {void}
+     */
+    function _onClick() {
+        dispatch(sendReaction(reaction));
+    }
+
+    return (
+        <TouchableHighlight
+            accessibilityLabel = { t(`toolbar.accessibilityLabel.${reaction}`) }
+            accessibilityRole = 'button'
+            onPress = { _onClick }
+            style = { styles.style }
+            underlayColor = { styles.underlayColor }>
+            <Text style = { styles.emoji }>{REACTIONS[reaction].emoji}</Text>
+        </TouchableHighlight>
+    );
+}
+
+export default translate(ReactionButton);
diff --git a/react/features/reactions/components/native/ReactionEmoji.js b/react/features/reactions/components/native/ReactionEmoji.js
new file mode 100644
index 000000000..85d3c464f
--- /dev/null
+++ b/react/features/reactions/components/native/ReactionEmoji.js
@@ -0,0 +1,96 @@
+// @flow
+
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { Animated } from 'react-native';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
+import { removeReaction } from '../../actions.any';
+import { REACTIONS, type ReactionEmojiProps } from '../../constants';
+
+
+type Props = ReactionEmojiProps & {
+
+    /**
+     * Index of reaction on the queue.
+     * Used to differentiate between first and other animations.
+     */
+    index: number
+};
+
+
+/**
+ * Animated reaction emoji.
+ *
+ * @returns {ReactElement}
+ */
+function ReactionEmoji({ reaction, uid, index }: Props) {
+    const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
+    const _height = useSelector(state => state['features/base/responsive-ui'].clientHeight);
+    const dispatch = useDispatch();
+
+    const animationVal = useRef(new Animated.Value(0)).current;
+
+    const vh = useState(_height / 100)[0];
+
+    const randomInt = (min, max) => Math.floor((Math.random() * (max - min + 1)) + min);
+
+    const animationIndex = useMemo(() => index % 21, [ index ]);
+
+    const coordinates = useState({
+        topX: animationIndex === 0 ? 40 : randomInt(-100, 100),
+        topY: animationIndex === 0 ? -70 : randomInt(-65, -75),
+        bottomX: animationIndex === 0 ? 140 : randomInt(150, 200),
+        bottomY: animationIndex === 0 ? -50 : randomInt(-40, -50)
+    })[0];
+
+
+    useEffect(() => {
+        setTimeout(() => dispatch(removeReaction(uid)), 5000);
+    }, []);
+
+    useEffect(() => {
+        Animated.timing(
+            animationVal,
+            {
+                toValue: 1,
+                duration: 5000,
+                useNativeDriver: true
+            }
+        ).start();
+    }, [ animationVal ]);
+
+
+    return (
+        <Animated.Text
+            style = {{
+                ..._styles.emojiAnimation,
+                transform: [
+                    { translateY: animationVal.interpolate({
+                        inputRange: [ 0, 0.70, 0.75, 1 ],
+                        outputRange: [ 0, coordinates.topY * vh, coordinates.topY * vh, coordinates.bottomY * vh ]
+                    })
+                    }, {
+                        translateX: animationVal.interpolate({
+                            inputRange: [ 0, 0.70, 0.75, 1 ],
+                            outputRange: [ 0, coordinates.topX, coordinates.topX,
+                                coordinates.topX < 0 ? -coordinates.bottomX : coordinates.bottomX ]
+                        })
+                    }, {
+                        scale: animationVal.interpolate({
+                            inputRange: [ 0, 0.70, 0.75, 1 ],
+                            outputRange: [ 0.6, 1.5, 1.5, 1 ]
+                        })
+                    }
+                ],
+                opacity: animationVal.interpolate({
+                    inputRange: [ 0, 0.7, 0.75, 1 ],
+                    outputRange: [ 1, 1, 1, 0 ]
+                })
+            }}>
+            {REACTIONS[reaction].emoji}
+        </Animated.Text>
+    );
+}
+
+export default ReactionEmoji;
diff --git a/react/features/reactions/components/native/ReactionMenu.js b/react/features/reactions/components/native/ReactionMenu.js
new file mode 100644
index 000000000..91dc6bf61
--- /dev/null
+++ b/react/features/reactions/components/native/ReactionMenu.js
@@ -0,0 +1,59 @@
+// @flow
+
+import React from 'react';
+import { View } from 'react-native';
+import { useSelector } from 'react-redux';
+
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
+import { getParticipantCount } from '../../../base/participants';
+import { REACTIONS } from '../../constants';
+
+import RaiseHandButton from './RaiseHandButton';
+import ReactionButton from './ReactionButton';
+
+/**
+ * The type of the React {@code Component} props of {@link ReactionMenu}.
+ */
+type Props = {
+
+    /**
+     * Used to close the overflow menu after raise hand is clicked.
+     */
+    onCancel: Function,
+
+    /**
+     * Whether or not it's displayed in the overflow menu.
+     */
+    overflowMenu: boolean
+};
+
+/**
+ * Animated reaction emoji.
+ *
+ * @returns {ReactElement}
+ */
+function ReactionMenu({
+    onCancel,
+    overflowMenu
+}: Props) {
+    const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
+    const _participantCount = useSelector(state => getParticipantCount(state));
+
+    return (
+        <View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
+            {_participantCount > 1
+                && <View style = { _styles.reactionRow }>
+                    {Object.keys(REACTIONS).map(key => (
+                        <ReactionButton
+                            key = { key }
+                            reaction = { key }
+                            styles = { _styles.reactionButton } />
+                    ))}
+                </View>
+            }
+            <RaiseHandButton onCancel = { onCancel } />
+        </View>
+    );
+}
+
+export default ReactionMenu;
diff --git a/react/features/reactions/components/native/ReactionMenuDialog.js b/react/features/reactions/components/native/ReactionMenuDialog.js
new file mode 100644
index 000000000..c1faf4936
--- /dev/null
+++ b/react/features/reactions/components/native/ReactionMenuDialog.js
@@ -0,0 +1,143 @@
+// @flow
+
+import React, { PureComponent } from 'react';
+import { SafeAreaView, TouchableWithoutFeedback, View } from 'react-native';
+
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
+import { hideDialog, isDialogOpen } from '../../../base/dialog';
+import { getParticipantCount } from '../../../base/participants';
+import { connect } from '../../../base/redux';
+import type { StyleType } from '../../../base/styles';
+
+import ReactionMenu from './ReactionMenu';
+
+/**
+ * The type of the React {@code Component} props of {@link ReactionMenuDialog}.
+ */
+type Props = {
+
+    /**
+     * The color-schemed stylesheet of the feature.
+     */
+    _styles: StyleType,
+
+    /**
+     * True if the dialog is currently visible, false otherwise.
+     */
+    _isOpen: boolean,
+
+    /**
+     * The width of the screen.
+     */
+    _width: number,
+
+    /**
+    * The height of the screen.
+    */
+    _height: number,
+
+    /**
+     * Number of conference participants.
+     */
+    _participantCount: number,
+
+    /**
+     * Used for hiding the dialog when the selection was completed.
+     */
+    dispatch: Function
+};
+
+/**
+ * The exported React {@code Component}. We need it to execute
+ * {@link hideDialog}.
+ *
+ * XXX It does not break our coding style rule to not utilize globals for state,
+ * because it is merely another name for {@code export}'s {@code default}.
+ */
+let ReactionMenu_; // eslint-disable-line prefer-const
+
+/**
+ * Implements a React {@code Component} with some extra actions in addition to
+ * those in the toolbar.
+ */
+class ReactionMenuDialog extends PureComponent<Props> {
+    /**
+     * Initializes a new {@code ReactionMenuDialog} instance.
+     *
+     * @inheritdoc
+     */
+    constructor(props: Props) {
+        super(props);
+
+        // Bind event handlers so they are only bound once per instance.
+        this._onCancel = this._onCancel.bind(this);
+    }
+
+    /**
+     * Implements React's {@link Component#render()}.
+     *
+     * @inheritdoc
+     * @returns {ReactElement}
+     */
+    render() {
+        const { _styles, _width, _height, _participantCount } = this.props;
+
+        return (
+            <SafeAreaView style = { _styles }>
+                <TouchableWithoutFeedback
+                    onPress = { this._onCancel }>
+                    <View style = { _styles }>
+                        <View
+                            style = {{
+                                left: (_width - 360) / 2,
+                                top: _height - (_participantCount > 1 ? 144 : 80) - 80
+                            }}>
+                            <ReactionMenu
+                                onCancel = { this._onCancel }
+                                overflowMenu = { false } />
+                        </View>
+                    </View>
+                </TouchableWithoutFeedback>
+            </SafeAreaView>
+        );
+    }
+
+    _onCancel: () => boolean;
+
+    /**
+     * Hides this {@code ReactionMenuDialog}.
+     *
+     * @private
+     * @returns {boolean}
+     */
+    _onCancel() {
+        if (this.props._isOpen) {
+            this.props.dispatch(hideDialog(ReactionMenu_));
+
+            return true;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Function that maps parts of Redux state tree into component props.
+ *
+ * @param {Object} state - Redux state.
+ * @private
+ * @returns {Props}
+ */
+function _mapStateToProps(state) {
+    return {
+        _isOpen: isDialogOpen(state, ReactionMenu_),
+        _styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
+        _width: state['features/base/responsive-ui'].clientWidth,
+        _height: state['features/base/responsive-ui'].clientHeight,
+        _participantCount: getParticipantCount(state)
+    };
+}
+
+ReactionMenu_ = connect(_mapStateToProps)(ReactionMenuDialog);
+
+export default ReactionMenu_;
diff --git a/react/features/toolbox/components/native/RaiseHandButton.js b/react/features/reactions/components/native/ReactionsMenuButton.js
similarity index 61%
rename from react/features/toolbox/components/native/RaiseHandButton.js
rename to react/features/reactions/components/native/ReactionsMenuButton.js
index cdbf4fd43..546a77159 100644
--- a/react/features/toolbox/components/native/RaiseHandButton.js
+++ b/react/features/reactions/components/native/ReactionsMenuButton.js
@@ -2,35 +2,33 @@
 
 import { type Dispatch } from 'redux';
 
-import {
-    createToolbarEvent,
-    sendAnalytics
-} from '../../../analytics';
+import { isDialogOpen, openDialog } from '../../../base/dialog';
 import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
 import { translate } from '../../../base/i18n';
 import { IconRaisedHand } from '../../../base/icons';
 import {
-    getLocalParticipant,
-    raiseHand
+    getLocalParticipant
 } from '../../../base/participants';
 import { connect } from '../../../base/redux';
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
 
+import ReactionMenuDialog from './ReactionMenuDialog';
+
 /**
- * The type of the React {@code Component} props of {@link RaiseHandButton}.
+ * The type of the React {@code Component} props of {@link ReactionsMenuButton}.
  */
 type Props = AbstractButtonProps & {
 
     /**
-     * The local participant.
-     */
-    _localParticipant: Object,
-
-    /**
-     * Whether the participant raused their hand or not.
+     * Whether the participant raised their hand or not.
      */
     _raisedHand: boolean,
 
+    /**
+     * Whether or not the reactions menu is open.
+     */
+    _reactionsOpen: boolean,
+
     /**
      * The redux {@code dispatch} function.
      */
@@ -40,11 +38,11 @@ type Props = AbstractButtonProps & {
 /**
  * An implementation of a button to raise or lower hand.
  */
-class RaiseHandButton extends AbstractButton<Props, *> {
-    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
+class ReactionsMenuButton extends AbstractButton<Props, *> {
+    accessibilityLabel = 'toolbar.accessibilityLabel.reactionsMenu';
     icon = IconRaisedHand;
-    label = 'toolbar.raiseYourHand';
-    toggledLabel = 'toolbar.lowerYourHand';
+    label = 'toolbar.openReactionsMenu';
+    toggledLabel = 'toolbar.closeReactionsMenu';
 
     /**
      * Handles clicking / pressing the button.
@@ -54,7 +52,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
      * @returns {void}
      */
     _handleClick() {
-        this._toggleRaisedHand();
+        this.props.dispatch(openDialog(ReactionMenuDialog));
     }
 
     /**
@@ -65,20 +63,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
      * @returns {boolean}
      */
     _isToggled() {
-        return this.props._raisedHand;
-    }
-
-    /**
-     * Toggles the rased hand status of the local participant.
-     *
-     * @returns {void}
-     */
-    _toggleRaisedHand() {
-        const enable = !this.props._raisedHand;
-
-        sendAnalytics(createToolbarEvent('raise.hand', { enable }));
-
-        this.props.dispatch(raiseHand(enable));
+        return this.props._raisedHand || this.props._reactionsOpen;
     }
 }
 
@@ -96,10 +81,10 @@ function _mapStateToProps(state, ownProps): Object {
     const { visible = enabled } = ownProps;
 
     return {
-        _localParticipant,
         _raisedHand: _localParticipant.raisedHand,
+        _reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
         visible
     };
 }
 
-export default translate(connect(_mapStateToProps)(RaiseHandButton));
+export default translate(connect(_mapStateToProps)(ReactionsMenuButton));
diff --git a/react/features/reactions/components/native/index.js b/react/features/reactions/components/native/index.js
new file mode 100644
index 000000000..4a2bef910
--- /dev/null
+++ b/react/features/reactions/components/native/index.js
@@ -0,0 +1,3 @@
+export { default as ReactionsMenuButton } from './ReactionsMenuButton';
+export { default as ReactionEmoji } from './ReactionEmoji';
+export { default as ReactionMenu } from './ReactionMenu';
diff --git a/react/features/reactions/components/web/ReactionButton.js b/react/features/reactions/components/web/ReactionButton.js
new file mode 100644
index 000000000..3a7ed0529
--- /dev/null
+++ b/react/features/reactions/components/web/ReactionButton.js
@@ -0,0 +1,125 @@
+/* @flow */
+
+import React from 'react';
+
+import { Tooltip } from '../../../base/tooltip';
+import AbstractToolbarButton from '../../../toolbox/components/AbstractToolbarButton';
+import type { Props as AbstractToolbarButtonProps } from '../../../toolbox/components/AbstractToolbarButton';
+
+/**
+ * The type of the React {@code Component} props of {@link ReactionButton}.
+ */
+type Props = AbstractToolbarButtonProps & {
+
+    /**
+     * Optional text to display in the tooltip.
+     */
+    tooltip?: string,
+
+    /**
+     * From which direction the tooltip should appear, relative to the
+     * button.
+     */
+    tooltipPosition: string,
+
+    /**
+     * Optional label for the button
+     */
+    label?: string
+};
+
+/**
+ * Represents a button in the reactions menu.
+ *
+ * @extends AbstractToolbarButton
+ */
+class ReactionButton extends AbstractToolbarButton<Props> {
+    /**
+     * Default values for {@code ReactionButton} component's properties.
+     *
+     * @static
+     */
+    static defaultProps = {
+        tooltipPosition: 'top'
+    };
+
+    /**
+     * Initializes a new {@code ReactionButton} instance.
+     *
+     * @inheritdoc
+     */
+    constructor(props: Props) {
+        super(props);
+
+        this._onKeyDown = this._onKeyDown.bind(this);
+    }
+
+    _onKeyDown: (Object) => void;
+
+    /**
+     * Handles 'Enter' key on the button to trigger onClick for accessibility.
+     * We should be handling Space onKeyUp but it conflicts with PTT.
+     *
+     * @param {Object} event - The key event.
+     * @private
+     * @returns {void}
+     */
+    _onKeyDown(event) {
+        // If the event coming to the dialog has been subject to preventDefault
+        // we don't handle it here.
+        if (event.defaultPrevented) {
+            return;
+        }
+
+        if (event.key === 'Enter') {
+            event.preventDefault();
+            event.stopPropagation();
+            this.props.onClick();
+        }
+    }
+
+    /**
+     * Renders the button of this {@code ReactionButton}.
+     *
+     * @param {Object} children - The children, if any, to be rendered inside
+     * the button. Presumably, contains the emoji of this {@code ReactionButton}.
+     * @protected
+     * @returns {ReactElement} The button of this {@code ReactionButton}.
+     */
+    _renderButton(children) {
+        return (
+            <div
+                aria-label = { this.props.accessibilityLabel }
+                aria-pressed = { this.props.toggled }
+                className = 'toolbox-button'
+                onClick = { this.props.onClick }
+                onKeyDown = { this._onKeyDown }
+                role = 'button'
+                tabIndex = { 0 }>
+                { this.props.tooltip
+                    ? <Tooltip
+                        content = { this.props.tooltip }
+                        position = { this.props.tooltipPosition }>
+                        { children }
+                    </Tooltip>
+                    : children }
+            </div>
+        );
+    }
+
+    /**
+     * Renders the icon (emoji) of this {@code reactionButton}.
+     *
+     * @inheritdoc
+     */
+    _renderIcon() {
+        return (
+            <div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
+                <span className = 'emoji'>{this.props.icon}</span>
+                {this.props.label && <span className = 'text'>{this.props.label}</span>}
+            </div>
+        );
+    }
+}
+
+export default ReactionButton;
diff --git a/react/features/reactions/components/web/ReactionEmoji.js b/react/features/reactions/components/web/ReactionEmoji.js
new file mode 100644
index 000000000..deba0907d
--- /dev/null
+++ b/react/features/reactions/components/web/ReactionEmoji.js
@@ -0,0 +1,96 @@
+// @flow
+
+import React, { Component } from 'react';
+
+import { connect } from '../../../base/redux';
+import { removeReaction } from '../../actions.any';
+import { REACTIONS } from '../../constants';
+
+type Props = {
+
+    /**
+     * Reaction to be displayed.
+     */
+    reaction: string,
+
+    /**
+     * Id of the reaction.
+     */
+    uid: Number,
+
+    /**
+     * Removes reaction from redux state.
+     */
+    removeReaction: Function,
+
+    /**
+     * Index of the reaction in the queue.
+     */
+    index: number
+};
+
+type State = {
+
+    /**
+     * Index of CSS animation. Number between 0-20.
+     */
+    index: number
+}
+
+
+/**
+ * Used to display animated reactions.
+ *
+ * @returns {ReactElement}
+ */
+class ReactionEmoji extends Component<Props, State> {
+    /**
+     * Initializes a new {@code ReactionEmoji} instance.
+     *
+     * @param {Props} props - The read-only React {@code Component} props with
+     * which the new instance is to be initialized.
+     */
+    constructor(props: Props) {
+        super(props);
+
+        this.state = {
+            index: props.index % 21
+        };
+    }
+
+    /**
+     * Implements React Component's componentDidMount.
+     *
+     * @inheritdoc
+     */
+    componentDidMount() {
+        setTimeout(() => this.props.removeReaction(this.props.uid), 5000);
+    }
+
+    /**
+     * Implements React's {@link Component#render}.
+     *
+     * @inheritdoc
+     */
+    render() {
+        const { reaction, uid } = this.props;
+        const { index } = this.state;
+
+        return (
+            <div
+                className = { `reaction-emoji reaction-${index}` }
+                id = { uid }>
+                { REACTIONS[reaction].emoji }
+            </div>
+        );
+    }
+}
+
+const mapDispatchToProps = {
+    removeReaction
+};
+
+export default connect(
+    null,
+    mapDispatchToProps,
+)(ReactionEmoji);
diff --git a/react/features/reactions/components/web/ReactionsMenu.js b/react/features/reactions/components/web/ReactionsMenu.js
new file mode 100644
index 000000000..113d4aab4
--- /dev/null
+++ b/react/features/reactions/components/web/ReactionsMenu.js
@@ -0,0 +1,233 @@
+// @flow
+
+import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
+
+import {
+    createToolbarEvent,
+    sendAnalytics
+} from '../../../analytics';
+import { translate } from '../../../base/i18n';
+import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants';
+import { connect } from '../../../base/redux';
+import { dockToolbox } from '../../../toolbox/actions.web';
+import { sendReaction } from '../../actions.any';
+import { toggleReactionsMenuVisibility } from '../../actions.web';
+import { REACTIONS } from '../../constants';
+
+import ReactionButton from './ReactionButton';
+
+type Props = {
+
+    /**
+     * The number of conference participants.
+     */
+    _participantCount: number,
+
+    /**
+     * Used for translation.
+     */
+    t: Function,
+
+    /**
+     * Whether or not the local participant's hand is raised.
+     */
+    _raisedHand: boolean,
+
+    /**
+     * The ID of the local participant.
+     */
+    _localParticipantID: String,
+
+    /**
+     * The Redux Dispatch function.
+     */
+    dispatch: Function,
+
+    /**
+     * Docks the toolbox
+     */
+    _dockToolbox: Function,
+
+    /**
+     * Whether or not it's displayed in the overflow menu.
+     */
+    overflowMenu: boolean
+};
+
+declare var APP: Object;
+
+/**
+ * Implements the reactions menu.
+ *
+ * @returns {ReactElement}
+ */
+class ReactionsMenu extends Component<Props> {
+    /**
+     * Initializes a new {@code ReactionsMenu} instance.
+     *
+     * @param {Props} props - The read-only React {@code Component} props with
+     * which the new instance is to be initialized.
+     */
+    constructor(props: Props) {
+        super(props);
+
+        this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
+        this._getReactionButtons = this._getReactionButtons.bind(this);
+    }
+
+    _onToolbarToggleRaiseHand: () => void;
+
+    _getReactionButtons: () => Array<React$Element<*>>;
+
+    /**
+     * Implements React Component's componentDidMount.
+     *
+     * @inheritdoc
+     */
+    componentDidMount() {
+        this.props._dockToolbox(true);
+    }
+
+    /**
+     * Implements React Component's componentWillUnmount.
+     *
+     * @inheritdoc
+     */
+    componentWillUnmount() {
+        this.props._dockToolbox(false);
+    }
+
+    /**
+     * Creates an analytics toolbar event and dispatches an action for toggling
+     * raise hand.
+     *
+     * @returns {void}
+     */
+    _onToolbarToggleRaiseHand() {
+        sendAnalytics(createToolbarEvent(
+            'raise.hand',
+            { enable: !this.props._raisedHand }));
+        this._doToggleRaiseHand();
+        this.props.dispatch(toggleReactionsMenuVisibility());
+    }
+
+    /**
+     * Dispatches an action to toggle the local participant's raised hand state.
+     *
+     * @private
+     * @returns {void}
+     */
+    _doToggleRaiseHand() {
+        const { _localParticipantID, _raisedHand } = this.props;
+        const newRaisedStatus = !_raisedHand;
+
+        this.props.dispatch(participantUpdated({
+            // XXX Only the local participant is allowed to update without
+            // stating the JitsiConference instance (i.e. participant property
+            // `conference` for a remote participant) because the local
+            // participant is uniquely identified by the very fact that there is
+            // only one local participant.
+
+            id: _localParticipantID,
+            local: true,
+            raisedHand: newRaisedStatus
+        }));
+
+        APP.API.notifyRaiseHandUpdated(_localParticipantID, newRaisedStatus);
+    }
+
+    /**
+     * Returns the emoji reaction buttons.
+     *
+     * @returns {Array}
+     */
+    _getReactionButtons() {
+        const { t, dispatch } = this.props;
+
+        return Object.keys(REACTIONS).map(key => {
+            /**
+             * Sends reaction message.
+             *
+             * @returns {void}
+             */
+            function sendMessage() {
+                dispatch(sendReaction(key));
+            }
+
+            return (<ReactionButton
+                accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
+                icon = { REACTIONS[key].emoji }
+                key = { key }
+                onClick = { sendMessage }
+                toggled = { false }
+                tooltip = { t(`toolbar.${key}`) } />);
+        });
+    }
+
+    /**
+     * Implements React's {@link Component#render}.
+     *
+     * @inheritdoc
+     */
+    render() {
+        const { _participantCount, _raisedHand, t, overflowMenu } = this.props;
+
+        return (
+            <div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
+                { _participantCount > 1 && <div className = 'reactions-row'>
+                    { this._getReactionButtons() }
+                </div> }
+                <div className = 'raise-hand-row'>
+                    <ReactionButton
+                        accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
+                        icon = '✋'
+                        key = 'raisehand'
+                        label = {
+                            `${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
+                            ${overflowMenu ? '' : ' (R)'}`
+                        }
+                        onClick = { this._onToolbarToggleRaiseHand }
+                        toggled = { true } />
+                </div>
+            </div>
+        );
+    }
+}
+
+/**
+ * Function that maps parts of Redux state tree into component props.
+ *
+ * @param {Object} state - Redux state.
+ * @returns {Object}
+ */
+function mapStateToProps(state) {
+    const localParticipant = getLocalParticipant(state);
+
+    return {
+        _localParticipantID: localParticipant.id,
+        _raisedHand: localParticipant.raisedHand,
+        _participantCount: getParticipantCount(state)
+    };
+}
+
+/**
+ * Function that maps parts of Redux actions into component props.
+ *
+ * @param {Object} dispatch - Redux dispatch.
+ * @returns {Object}
+ */
+function mapDispatchToProps(dispatch) {
+    return {
+        dispatch,
+        ...bindActionCreators(
+        {
+            _dockToolbox: dockToolbox
+        }, dispatch)
+    };
+}
+
+export default translate(connect(
+    mapStateToProps,
+    mapDispatchToProps,
+)(ReactionsMenu));
diff --git a/react/features/reactions/components/web/ReactionsMenuButton.js b/react/features/reactions/components/web/ReactionsMenuButton.js
new file mode 100644
index 000000000..2b8c797b7
--- /dev/null
+++ b/react/features/reactions/components/web/ReactionsMenuButton.js
@@ -0,0 +1,139 @@
+// @flow
+
+import React, { useEffect } from 'react';
+
+import { translate } from '../../../base/i18n';
+import { IconRaisedHand } from '../../../base/icons';
+import { getLocalParticipant } from '../../../base/participants';
+import { connect } from '../../../base/redux';
+import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
+import { sendReaction } from '../../actions.any';
+import { toggleReactionsMenuVisibility } from '../../actions.web';
+import { REACTIONS, type ReactionEmojiProps } from '../../constants';
+import { getReactionsQueue } from '../../functions.any';
+import { getReactionsMenuVisibility } from '../../functions.web';
+
+import ReactionEmoji from './ReactionEmoji';
+import ReactionsMenuPopup from './ReactionsMenuPopup';
+
+type Props = {
+
+    /**
+     * Used for translation.
+     */
+    t: Function,
+
+    /**
+     * Whether or not the local participant's hand is raised.
+     */
+    raisedHand: boolean,
+
+    /**
+     * Click handler for the reaction button. Toggles the reactions menu.
+     */
+    onReactionsClick: Function,
+
+    /**
+     * Whether or not the reactions menu is open.
+     */
+    isOpen: boolean,
+
+    /**
+     * The array of reactions to be displayed.
+     */
+    reactionsQueue: Array<ReactionEmojiProps>,
+
+    /**
+     * Redux dispatch function.
+     */
+    dispatch: Function
+};
+
+
+declare var APP: Object;
+
+/**
+ * Button used for the reactions menu.
+ *
+ * @returns {ReactElement}
+ */
+function ReactionsMenuButton({
+    t,
+    raisedHand,
+    isOpen,
+    reactionsQueue,
+    dispatch
+}: Props) {
+
+    useEffect(() => {
+        const KEYBOARD_SHORTCUTS = Object.keys(REACTIONS).map(key => {
+            return {
+                character: REACTIONS[key].shortcutChar,
+                exec: () => dispatch(sendReaction(key)),
+                helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
+                altKey: true
+            };
+        });
+
+        KEYBOARD_SHORTCUTS.forEach(shortcut => {
+            APP.keyboardshortcut.registerShortcut(
+                shortcut.character,
+                null,
+                shortcut.exec,
+                shortcut.helpDescription,
+                shortcut.altKey);
+        });
+
+        return () => {
+            Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
+                .forEach(letter =>
+                    APP.keyboardshortcut.unregisterShortcut(letter, true));
+        };
+    }, []);
+
+    /**
+     * Toggles the reactions menu visibility.
+     *
+     * @returns {void}
+     */
+    function toggleReactionsMenu() {
+        dispatch(toggleReactionsMenuVisibility());
+    }
+
+    return (
+        <div className = 'reactions-menu-popup-container'>
+            <ReactionsMenuPopup>
+                <ToolbarButton
+                    accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
+                    icon = { IconRaisedHand }
+                    key = 'reactions'
+                    onClick = { toggleReactionsMenu }
+                    toggled = { raisedHand }
+                    tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
+            </ReactionsMenuPopup>
+            {reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
+                index = { index }
+                key = { uid }
+                reaction = { reaction }
+                uid = { uid } />))}
+        </div>
+    );
+}
+
+/**
+ * Function that maps parts of Redux state tree into component props.
+ *
+ * @param {Object} state - Redux state.
+ * @returns {Object}
+ */
+function mapStateToProps(state) {
+    const localParticipant = getLocalParticipant(state);
+
+    return {
+        isOpen: getReactionsMenuVisibility(state),
+        reactionsQueue: getReactionsQueue(state),
+        raisedHand: localParticipant?.raisedHand
+    };
+}
+
+export default translate(connect(mapStateToProps)(ReactionsMenuButton));
diff --git a/react/features/reactions/components/web/ReactionsMenuPopup.js b/react/features/reactions/components/web/ReactionsMenuPopup.js
new file mode 100644
index 000000000..b328a71fd
--- /dev/null
+++ b/react/features/reactions/components/web/ReactionsMenuPopup.js
@@ -0,0 +1,58 @@
+// @flow
+
+import InlineDialog from '@atlaskit/inline-dialog';
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { toggleReactionsMenuVisibility } from '../../actions.web';
+import { getReactionsMenuVisibility } from '../../functions.web';
+
+import ReactionsMenu from './ReactionsMenu';
+
+
+type Props = {
+
+    /**
+    * Component's children (the reactions menu button).
+    */
+    children: React$Node
+}
+
+/**
+ * Popup with reactions menu.
+ *
+ * @returns {ReactElement}
+ */
+function ReactionsMenuPopup({
+    children
+}: Props) {
+    /**
+    * Flag controlling the visibility of the popup.
+    */
+    const isOpen = useSelector(state => getReactionsMenuVisibility(state));
+
+    const dispatch = useDispatch();
+
+    /**
+     * Toggles reactions menu visibility.
+     *
+     * @returns {void}
+     */
+    function onClose() {
+        dispatch(toggleReactionsMenuVisibility());
+    }
+
+    return (
+        <div className = 'reactions-menu-popup'>
+            <InlineDialog
+                content = { <ReactionsMenu /> }
+                isOpen = { isOpen }
+                onClose = { onClose }
+                placement = 'top'>
+                {children}
+            </InlineDialog>
+        </div>
+    );
+}
+
+export default ReactionsMenuPopup;
diff --git a/react/features/reactions/components/web/index.js b/react/features/reactions/components/web/index.js
new file mode 100644
index 000000000..c63636541
--- /dev/null
+++ b/react/features/reactions/components/web/index.js
@@ -0,0 +1,7 @@
+// @flow
+
+export { default as ReactionButton } from './ReactionButton';
+export { default as ReactionEmoji } from './ReactionEmoji';
+export { default as ReactionsMenu } from './ReactionsMenu';
+export { default as ReactionsMenuButton } from './ReactionsMenuButton';
+export { default as ReactionsMenuPopup } from './ReactionsMenuPopup';
diff --git a/react/features/reactions/constants.js b/react/features/reactions/constants.js
new file mode 100644
index 000000000..8708b2b8a
--- /dev/null
+++ b/react/features/reactions/constants.js
@@ -0,0 +1,47 @@
+// @flow
+
+export const REACTIONS = {
+    clap: {
+        message: ':clap:',
+        emoji: '👏',
+        shortcutChar: 'C'
+    },
+    like: {
+        message: ':thumbs_up:',
+        emoji: '👍',
+        shortcutChar: 'T'
+    },
+    smile: {
+        message: ':smile:',
+        emoji: '😀',
+        shortcutChar: 'S'
+    },
+    joy: {
+        message: ':joy:',
+        emoji: '😂',
+        shortcutChar: 'L'
+    },
+    surprised: {
+        message: ':face_with_open_mouth:',
+        emoji: '😮',
+        shortcutChar: 'O'
+    },
+    party: {
+        message: ':party_popper:',
+        emoji: '🎉',
+        shortcutChar: 'P'
+    }
+};
+
+export type ReactionEmojiProps = {
+
+    /**
+     * Reaction to be displayed.
+     */
+    reaction: string,
+
+    /**
+     * Id of the reaction.
+     */
+    uid: number
+}
diff --git a/react/features/reactions/functions.any.js b/react/features/reactions/functions.any.js
new file mode 100644
index 000000000..36ea78600
--- /dev/null
+++ b/react/features/reactions/functions.any.js
@@ -0,0 +1,11 @@
+// @flow
+
+/**
+ * Returns the queue of reactions.
+ *
+ * @param {Object} state - The state of the application.
+ * @returns {boolean}
+ */
+export function getReactionsQueue(state: Object) {
+    return state['features/reactions'].queue;
+}
diff --git a/react/features/reactions/functions.web.js b/react/features/reactions/functions.web.js
new file mode 100644
index 000000000..60c89adc0
--- /dev/null
+++ b/react/features/reactions/functions.web.js
@@ -0,0 +1,11 @@
+// @flow
+
+/**
+ * Returns the visibility state of the reactions menu.
+ *
+ * @param {Object} state - The state of the application.
+ * @returns {boolean}
+ */
+export function getReactionsMenuVisibility(state: Object) {
+    return state['features/reactions'].visible;
+}
diff --git a/react/features/reactions/middleware.js b/react/features/reactions/middleware.js
new file mode 100644
index 000000000..0c928e025
--- /dev/null
+++ b/react/features/reactions/middleware.js
@@ -0,0 +1,84 @@
+// @flow
+
+import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
+import { MiddlewareRegistry } from '../base/redux';
+
+import {
+    SET_REACTIONS_MESSAGE,
+    CLEAR_REACTIONS_MESSAGE,
+    SEND_REACTION,
+    PUSH_REACTION
+} from './actionTypes';
+import {
+    addReactionsMessage,
+    addReactionsMessageToChat,
+    flushReactionsToChat,
+    pushReaction,
+    setReactionQueue
+} from './actions.any';
+import { REACTIONS } from './constants';
+
+
+declare var APP: Object;
+
+/**
+ * Middleware which intercepts Reactions actions to handle changes to the
+ * visibility timeout of the Reactions.
+ *
+ * @param {Store} store - The redux store.
+ * @returns {Function}
+ */
+MiddlewareRegistry.register(store => next => action => {
+    const { dispatch, getState } = store;
+
+    switch (action.type) {
+    case SET_REACTIONS_MESSAGE: {
+        const { timeoutID, message } = getState()['features/reactions'];
+        const { reaction } = action;
+
+        clearTimeout(timeoutID);
+        action.message = `${message}${reaction}`;
+        action.timeoutID = setTimeout(() => {
+            dispatch(flushReactionsToChat());
+        }, 500);
+
+        break;
+    }
+
+    case CLEAR_REACTIONS_MESSAGE: {
+        const { message } = getState()['features/reactions'];
+
+        dispatch(addReactionsMessageToChat(message));
+
+        break;
+    }
+
+    case SEND_REACTION: {
+        const state = store.getState();
+        const { conference } = state['features/base/conference'];
+
+        if (conference) {
+            conference.sendEndpointMessage('', {
+                name: ENDPOINT_REACTION_NAME,
+                reaction: action.reaction,
+                timestamp: Date.now()
+            });
+            dispatch(addReactionsMessage(REACTIONS[action.reaction].message));
+            dispatch(pushReaction(action.reaction));
+        }
+        break;
+    }
+
+    case PUSH_REACTION: {
+        const queue = store.getState()['features/reactions'].queue;
+        const reaction = action.reaction;
+
+        dispatch(setReactionQueue([ ...queue, {
+            reaction,
+            uid: window.Date.now()
+        } ]));
+    }
+    }
+
+    return next(action);
+});
diff --git a/react/features/reactions/reducer.js b/react/features/reactions/reducer.js
new file mode 100644
index 000000000..23a5bf5c9
--- /dev/null
+++ b/react/features/reactions/reducer.js
@@ -0,0 +1,90 @@
+// @flow
+
+import { ReducerRegistry } from '../base/redux';
+
+import {
+    TOGGLE_REACTIONS_VISIBLE,
+    SET_REACTIONS_MESSAGE,
+    CLEAR_REACTIONS_MESSAGE,
+    SET_REACTION_QUEUE
+} from './actionTypes';
+
+/**
+ * Returns initial state for reactions' part of Redux store.
+ *
+ * @private
+ * @returns {{
+ *     visible: boolean,
+ *     message: string,
+ *     timeoutID: number,
+ *     queue: Array
+ * }}
+ */
+function _getInitialState() {
+    return {
+        /**
+         * The indicator that determines whether the reactions menu is visible.
+         *
+         * @type {boolean}
+         */
+        visible: false,
+
+        /**
+         * A string that contains the message to be added to the chat.
+         *
+         * @type {string}
+         */
+        message: '',
+
+        /**
+         * A number, non-zero value which identifies the timer created by a call
+         * to setTimeout().
+         *
+         * @type {number|null}
+         */
+        timeoutID: null,
+
+        /**
+         * The array of reactions to animate
+         *
+         * @type {Array}
+         */
+        queue: []
+    };
+}
+
+ReducerRegistry.register(
+    'features/reactions',
+    (state: Object = _getInitialState(), action: Object) => {
+        switch (action.type) {
+
+        case TOGGLE_REACTIONS_VISIBLE:
+            return {
+                ...state,
+                visible: !state.visible
+            };
+
+        case SET_REACTIONS_MESSAGE:
+            return {
+                ...state,
+                message: action.message,
+                timeoutID: action.timeoutID
+            };
+
+        case CLEAR_REACTIONS_MESSAGE:
+            return {
+                ...state,
+                message: '',
+                timeoutID: null
+            };
+
+        case SET_REACTION_QUEUE: {
+            return {
+                ...state,
+                queue: action.value
+            };
+        }
+        }
+
+        return state;
+    });
diff --git a/react/features/toolbox/components/native/MoreOptionsButton.js b/react/features/toolbox/components/native/MoreOptionsButton.js
deleted file mode 100644
index 735740266..000000000
--- a/react/features/toolbox/components/native/MoreOptionsButton.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// @flow
-
-import { translate } from '../../../base/i18n';
-import { IconMenu } from '../../../base/icons';
-import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
-
-
-type Props = AbstractButtonProps;
-
-/**
- * An implementation of a button to show more menu options.
- */
-class MoreOptionsButton extends AbstractButton<Props, any> {
-    accessibilityLabel = 'toolbar.accessibilityLabel.moreOptions';
-    icon = IconMenu;
-    label = 'toolbar.moreOptions';
-}
-
-
-export default translate(MoreOptionsButton);
diff --git a/react/features/toolbox/components/native/OverflowMenu.js b/react/features/toolbox/components/native/OverflowMenu.js
index 018cd81bf..a2ec33467 100644
--- a/react/features/toolbox/components/native/OverflowMenu.js
+++ b/react/features/toolbox/components/native/OverflowMenu.js
@@ -1,17 +1,15 @@
 // @flow
 
 import React, { PureComponent } from 'react';
-import { TouchableOpacity, View } from 'react-native';
-import Collapsible from 'react-native-collapsible';
 
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
 import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
-import { IconDragHandle } from '../../../base/icons';
 import { connect } from '../../../base/redux';
 import { StyleType } from '../../../base/styles';
 import { SharedDocumentButton } from '../../../etherpad';
 import { InviteButton } from '../../../invite';
 import { AudioRouteButton } from '../../../mobile/audio-mode';
+import { ReactionMenu } from '../../../reactions/components';
 import { LiveStreamButton, RecordButton } from '../../../recording';
 import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
 import { SharedVideoButton } from '../../../shared-video/components';
@@ -23,11 +21,8 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
 import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
 
 import AudioOnlyButton from './AudioOnlyButton';
-import MoreOptionsButton from './MoreOptionsButton';
-import RaiseHandButton from './RaiseHandButton';
 import ScreenSharingButton from './ScreenSharingButton.js';
 import ToggleCameraButton from './ToggleCameraButton';
-import styles from './styles';
 
 /**
  * The type of the React {@code Component} props of {@link OverflowMenu}.
@@ -65,12 +60,7 @@ type State = {
     /**
      * True if the bottom scheet is scrolled to the top.
      */
-    scrolledToTop: boolean,
-
-    /**
-     * True if the 'more' button set needas to be rendered.
-     */
-    showMore: boolean
+    scrolledToTop: boolean
 }
 
 /**
@@ -96,15 +86,12 @@ class OverflowMenu extends PureComponent<Props, State> {
         super(props);
 
         this.state = {
-            scrolledToTop: true,
-            showMore: false
+            scrolledToTop: true
         };
 
         // Bind event handlers so they are only bound once per instance.
         this._onCancel = this._onCancel.bind(this);
-        this._onSwipe = this._onSwipe.bind(this);
-        this._onToggleMenu = this._onToggleMenu.bind(this);
-        this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
+        this._renderReactionMenu = this._renderReactionMenu.bind(this);
     }
 
     /**
@@ -115,7 +102,6 @@ class OverflowMenu extends PureComponent<Props, State> {
      */
     render() {
         const { _bottomSheetStyles, _width } = this.props;
-        const { showMore } = this.state;
         const toolbarButtons = getMovableButtons(_width);
 
         const buttonProps = {
@@ -124,63 +110,45 @@ class OverflowMenu extends PureComponent<Props, State> {
             styles: _bottomSheetStyles.buttons
         };
 
-        const moreOptionsButtonProps = {
-            ...buttonProps,
-            afterClick: this._onToggleMenu,
-            visible: !showMore
+        const topButtonProps = {
+            afterClick: this._onCancel,
+            showLabel: true,
+            styles: {
+                ..._bottomSheetStyles.buttons,
+                style: {
+                    ..._bottomSheetStyles.buttons.style,
+                    borderTopLeftRadius: 16,
+                    borderTopRightRadius: 16,
+                    paddingTop: 16
+                }
+            }
         };
 
         return (
             <BottomSheet
                 onCancel = { this._onCancel }
-                onSwipe = { this._onSwipe }
-                renderHeader = { this._renderMenuExpandToggle }>
-                <AudioRouteButton { ...buttonProps } />
+                renderFooter = { toolbarButtons.has('raisehand')
+                    ? null
+                    : this._renderReactionMenu }>
+                <AudioRouteButton { ...topButtonProps } />
                 {!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
                 <AudioOnlyButton { ...buttonProps } />
-                {!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
                 <SecurityDialogButton { ...buttonProps } />
                 <ScreenSharingButton { ...buttonProps } />
-                <MoreOptionsButton { ...moreOptionsButtonProps } />
-                <Collapsible collapsed = { !showMore }>
-                    {!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
-                    {!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
-                    <RecordButton { ...buttonProps } />
-                    <LiveStreamButton { ...buttonProps } />
-                    <SharedVideoButton { ...buttonProps } />
-                    <ClosedCaptionButton { ...buttonProps } />
-                    <SharedDocumentButton { ...buttonProps } />
-                    <MuteEveryoneButton { ...buttonProps } />
-                    <MuteEveryonesVideoButton { ...buttonProps } />
-                    <HelpButton { ...buttonProps } />
-                </Collapsible>
+                {!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
+                {!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
+                <RecordButton { ...buttonProps } />
+                <LiveStreamButton { ...buttonProps } />
+                <SharedVideoButton { ...buttonProps } />
+                <ClosedCaptionButton { ...buttonProps } />
+                <SharedDocumentButton { ...buttonProps } />
+                <MuteEveryoneButton { ...buttonProps } />
+                <MuteEveryonesVideoButton { ...buttonProps } />
+                <HelpButton { ...buttonProps } />
             </BottomSheet>
         );
     }
 
-    _renderMenuExpandToggle: () => React$Element<any>;
-
-    /**
-     * Function to render the menu toggle in the bottom sheet header area.
-     *
-     * @returns {React$Element}
-     */
-    _renderMenuExpandToggle() {
-        return (
-            <View
-                style = { [
-                    this.props._bottomSheetStyles.sheet,
-                    styles.expandMenuContainer
-                ] }>
-                <TouchableOpacity onPress = { this._onToggleMenu }>
-                    { /* $FlowFixMe */ }
-                    <IconDragHandle
-                        fill = { this.props._bottomSheetStyles.buttons.iconStyle.color } />
-                </TouchableOpacity>
-            </View>
-        );
-    }
-
     _onCancel: () => boolean;
 
     /**
@@ -199,45 +167,17 @@ class OverflowMenu extends PureComponent<Props, State> {
         return false;
     }
 
-    _onSwipe: string => void;
+    _renderReactionMenu: () => React$Element<any>;
 
     /**
-     * Callback to be invoked when swipe gesture is detected on the menu. Returns true
-     * if the swipe gesture is handled by the menu, false otherwise.
+     * Functoin to render the reaction menu as the footer of the bottom sheet.
      *
-     * @param {string} direction - Direction of 'up' or 'down'.
-     * @returns {boolean}
+     * @returns {React$Element}
      */
-    _onSwipe(direction) {
-        const { showMore } = this.state;
-
-        switch (direction) {
-        case 'up':
-            !showMore && this.setState({
-                showMore: true
-            });
-
-            return !showMore;
-        case 'down':
-            showMore && this.setState({
-                showMore: false
-            });
-
-            return showMore;
-        }
-    }
-
-    _onToggleMenu: () => void;
-
-    /**
-     * Callback to be invoked when the expand menu button is pressed.
-     *
-     * @returns {void}
-     */
-    _onToggleMenu() {
-        this.setState({
-            showMore: !this.state.showMore
-        });
+    _renderReactionMenu() {
+        return (<ReactionMenu
+            onCancel = { this._onCancel }
+            overflowMenu = { true } />);
     }
 }
 
diff --git a/react/features/toolbox/components/native/Toolbox.js b/react/features/toolbox/components/native/Toolbox.js
index 01fa8fe54..86d3479c8 100644
--- a/react/features/toolbox/components/native/Toolbox.js
+++ b/react/features/toolbox/components/native/Toolbox.js
@@ -8,6 +8,7 @@ import { connect } from '../../../base/redux';
 import { StyleType } from '../../../base/styles';
 import { ChatButton } from '../../../chat';
 import { InviteButton } from '../../../invite';
+import { ReactionsMenuButton } from '../../../reactions/components';
 import { TileViewButton } from '../../../video-layout';
 import { isToolboxVisible, getMovableButtons } from '../../functions.native';
 import AudioMuteButton from '../AudioMuteButton';
@@ -15,7 +16,6 @@ import HangupButton from '../HangupButton';
 import VideoMuteButton from '../VideoMuteButton';
 
 import OverflowMenuButton from './OverflowMenuButton';
-import RaiseHandButton from './RaiseHandButton';
 import ToggleCameraButton from './ToggleCameraButton';
 import styles from './styles';
 
@@ -87,9 +87,9 @@ function Toolbox(props: Props) {
                           toggledStyles = { backgroundToggledStyle } />}
 
                 { additionalButtons.has('raisehand')
-                      && <RaiseHandButton
-                          styles = { buttonStylesBorderless }
-                          toggledStyles = { backgroundToggledStyle } />}
+                    && <ReactionsMenuButton
+                        styles = { buttonStylesBorderless }
+                        toggledStyles = { backgroundToggledStyle } />}
                 {additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
                 {additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
                 {additionalButtons.has('togglecamera')
diff --git a/react/features/toolbox/components/native/styles.js b/react/features/toolbox/components/native/styles.js
index de94407af..101e67b58 100644
--- a/react/features/toolbox/components/native/styles.js
+++ b/react/features/toolbox/components/native/styles.js
@@ -40,18 +40,38 @@ const whiteToolbarButtonIcon = {
     color: ColorPalette.white
 };
 
+/**
+ * The style of reaction buttons.
+ */
+const reactionButton = {
+    ...toolbarButton,
+    backgroundColor: 'transparent',
+    alignItems: 'center',
+    marginTop: 0,
+    marginHorizontal: 0
+};
+
+/**
+ * The style of the emoji on the reaction buttons.
+ */
+const reactionEmoji = {
+    fontSize: 20,
+    color: ColorPalette.white
+};
+
+const reactionMenu = {
+    flexDirection: 'column',
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: ColorPalette.black,
+    padding: 16
+};
+
 /**
  * The Toolbox and toolbar related styles.
  */
 const styles = {
 
-    expandMenuContainer: {
-        alignItems: 'center',
-        borderTopLeftRadius: 16,
-        borderTopRightRadius: 16,
-        flexDirection: 'column'
-    },
-
     sheetGestureRecognizer: {
         alignItems: 'stretch',
         flexDirection: 'column'
@@ -120,6 +140,67 @@ ColorSchemeRegistry.register('Toolbox', {
         underlayColor: ColorPalette.buttonUnderlay
     },
 
+    reactionDialog: {
+        position: 'absolute',
+        width: '100%',
+        height: '100%',
+        backgroundColor: 'transparent'
+    },
+
+    overflowReactionMenu: reactionMenu,
+
+    reactionMenu: {
+        ...reactionMenu,
+        borderRadius: 3,
+        width: 360
+    },
+
+    reactionRow: {
+        flexDirection: 'row',
+        justifyContent: 'space-between',
+        alignItems: 'center',
+        width: '100%',
+        marginBottom: 16
+    },
+
+    reactionButton: {
+        style: reactionButton,
+        underlayColor: ColorPalette.toggled,
+        emoji: reactionEmoji
+    },
+
+    raiseHandButton: {
+        style: {
+            ...reactionButton,
+            backgroundColor: ColorPalette.toggled,
+            width: '100%',
+            borderRadius: 6
+        },
+        underlayColor: ColorPalette.toggled,
+        emoji: reactionEmoji,
+        text: {
+            color: ColorPalette.white,
+            fontWeight: '600',
+            marginLeft: 8,
+            lineHeight: 24
+        },
+        container: {
+            flexDirection: 'row',
+            alignItems: 'center',
+            justifyContent: 'center'
+        }
+    },
+
+    emojiAnimation: {
+        color: ColorPalette.white,
+        position: 'absolute',
+        zIndex: 1001,
+        elevation: 2,
+        fontSize: 20,
+        left: '50%',
+        top: '100%'
+    },
+
     /**
      * Styles for toggled buttons in the toolbar.
      */
diff --git a/react/features/toolbox/components/web/Drawer.js b/react/features/toolbox/components/web/Drawer.js
index 2b9f12dc3..cc3838137 100644
--- a/react/features/toolbox/components/web/Drawer.js
+++ b/react/features/toolbox/components/web/Drawer.js
@@ -1,36 +1,24 @@
 // @flow
 
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useEffect, useRef } from 'react';
 
-import { translate } from '../../../base/i18n';
-import { Icon, IconArrowUpWide, IconArrowDownWide } from '../../../base/icons';
 
 type Props = {
 
-    /**
-     * Whether the drawer should have a button that expands its size or not.
-     */
-    canExpand: ?boolean,
-
     /**
      * The component(s) to be displayed within the drawer menu.
      */
     children: React$Node,
 
     /**
-     Whether the drawer should be shown or not.
+     * Whether the drawer should be shown or not.
      */
     isOpen: boolean,
 
     /**
-     Function that hides the drawer.
+     * Function that hides the drawer.
      */
-    onClose: Function,
-
-    /**
-     * Invoked to obtain translated strings.
-     */
-    t: Function
+    onClose: Function
 };
 
 /**
@@ -39,12 +27,9 @@ type Props = {
  * @returns {ReactElement}
  */
 function Drawer({
-    canExpand,
     children,
     isOpen,
-    onClose,
-    t }: Props) {
-    const [ expanded, setExpanded ] = useState(false);
+    onClose }: Props) {
     const drawerRef: Object = useRef(null);
 
     /**
@@ -69,53 +54,15 @@ function Drawer({
         };
     }, [ drawerRef ]);
 
-    /**
-     * Toggles the menu state between expanded/collapsed.
-     *
-     * @returns {void}
-     */
-    function toggleExpanded() {
-        setExpanded(!expanded);
-    }
-
-    /**
-     * KeyPress handler for accessibility.
-     *
-     * @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
-     *
-     * @returns {void}
-     */
-    function onKeyPress(e) {
-        if (e.key === ' ' || e.key === 'Enter') {
-            e.preventDefault();
-            toggleExpanded();
-        }
-    }
-
     return (
         isOpen ? (
             <div
-                className = { `drawer-menu${expanded ? ' expanded' : ''}` }
+                className = 'drawer-menu'
                 ref = { drawerRef }>
-                {canExpand && (
-                    <div
-                        aria-expanded = { expanded }
-                        aria-label = { expanded ? t('toolbar.accessibilityLabel.collapse')
-                            : t('toolbar.accessibilityLabel.expand') }
-                        className = 'drawer-toggle'
-                        onClick = { toggleExpanded }
-                        onKeyPress = { onKeyPress }
-                        role = 'button'
-                        tabIndex = { 0 }>
-                        <Icon
-                            size = { 24 }
-                            src = { expanded ? IconArrowDownWide : IconArrowUpWide } />
-                    </div>
-                )}
                 {children}
             </div>
         ) : null
     );
 }
 
-export default translate(Drawer);
+export default Drawer;
diff --git a/react/features/toolbox/components/web/OverflowMenuButton.js b/react/features/toolbox/components/web/OverflowMenuButton.js
index f032bee1a..71a78440a 100644
--- a/react/features/toolbox/components/web/OverflowMenuButton.js
+++ b/react/features/toolbox/components/web/OverflowMenuButton.js
@@ -7,6 +7,9 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
 import { translate } from '../../../base/i18n';
 import { IconHorizontalPoints } from '../../../base/icons';
 import { connect } from '../../../base/redux';
+import { ReactionEmoji, ReactionsMenu } from '../../../reactions/components';
+import { type ReactionEmojiProps } from '../../../reactions/constants';
+import { getReactionsQueue } from '../../../reactions/functions.any';
 
 import Drawer from './Drawer';
 import DrawerPortal from './DrawerPortal';
@@ -45,7 +48,17 @@ type Props = {
     /**
      * Invoked to obtain translated strings.
      */
-    t: Function
+    t: Function,
+
+    /**
+     * The array of reactions to be displayed.
+     */
+    reactionsQueue: Array<ReactionEmojiProps>,
+
+    /**
+     * Whether or not to display the reactions in the mobile menu.
+     */
+    showMobileReactions: boolean
 };
 
 /**
@@ -93,7 +106,7 @@ class OverflowMenuButton extends Component<Props> {
      * @returns {ReactElement}
      */
     render() {
-        const { children, isOpen, overflowDrawer } = this.props;
+        const { children, isOpen, overflowDrawer, reactionsQueue, showMobileReactions } = this.props;
 
         return (
             <div className = 'toolbox-button-wth-dialog'>
@@ -103,11 +116,18 @@ class OverflowMenuButton extends Component<Props> {
                             {this._renderToolbarButton()}
                             <DrawerPortal>
                                 <Drawer
-                                    canExpand = { true }
                                     isOpen = { isOpen }
                                     onClose = { this._onCloseDialog }>
                                     {children}
+                                    {showMobileReactions && <ReactionsMenu overflowMenu = { true } />}
                                 </Drawer>
+                                {showMobileReactions && <div className = 'reactions-animations-container'>
+                                    {reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
+                                        index = { index }
+                                        key = { uid }
+                                        reaction = { reaction }
+                                        uid = { uid } />))}
+                                </div>}
                             </DrawerPortal>
                         </>
                     ) : (
@@ -188,7 +208,8 @@ function mapStateToProps(state) {
     const { overflowDrawer } = state['features/toolbox'];
 
     return {
-        overflowDrawer
+        overflowDrawer,
+        reactionsQueue: getReactionsQueue(state)
     };
 }
 
diff --git a/react/features/toolbox/components/web/RaiseHandButton.js b/react/features/toolbox/components/web/RaiseHandButton.js
deleted file mode 100644
index 4ebdb3613..000000000
--- a/react/features/toolbox/components/web/RaiseHandButton.js
+++ /dev/null
@@ -1,83 +0,0 @@
-// @flow
-
-import { translate } from '../../../base/i18n';
-import { IconRaisedHand } from '../../../base/icons';
-import { getLocalParticipant } from '../../../base/participants';
-import { connect } from '../../../base/redux';
-import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
-
-type Props = AbstractButtonProps & {
-
-    /**
-     * Whether or not the local participant's hand is raised.
-     */
-     _raisedHand: boolean,
-
-     /**
-      * External handler for click action.
-      */
-     handleClick: Function
-};
-
-/**
- * Implementation of a button for toggling raise hand functionality.
- */
-class RaiseHandButton extends AbstractButton<Props, *> {
-    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
-    icon = IconRaisedHand
-    label = 'toolbar.raiseYourHand';
-    toggledLabel = 'toolbar.lowerYourHand'
-
-    /**
-     * Retrieves tooltip dynamically.
-     */
-    get tooltip() {
-        return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand';
-    }
-
-    /**
-     * Required by linter due to AbstractButton overwritten prop being writable.
-     *
-     * @param {string} value - The value.
-     */
-    set tooltip(value) {
-        return value;
-    }
-
-    /**
-     * Handles clicking / pressing the button, and opens the appropriate dialog.
-     *
-     * @protected
-     * @returns {void}
-     */
-    _handleClick() {
-        this.props.handleClick();
-    }
-
-    /**
-     * Indicates whether this button is in toggled state or not.
-     *
-     * @override
-     * @protected
-     * @returns {boolean}
-     */
-    _isToggled() {
-        return this.props._raisedHand;
-    }
-}
-
-/**
- * Function that maps parts of Redux state tree into component props.
- *
- * @param {Object} state - Redux state.
- * @returns {Object}
- */
-const mapStateToProps = state => {
-    const localParticipant = getLocalParticipant(state);
-
-    return {
-        _raisedHand: localParticipant.raisedHand
-    };
-};
-
-export default translate(connect(mapStateToProps)(RaiseHandButton));
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index f6d9de5e5..b53ed6a46 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -36,6 +36,7 @@ import {
 } from '../../../participants-pane/actions';
 import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
+import { ReactionsMenuButton } from '../../../reactions/components';
 import {
     LiveStreamButton,
     RecordButton
@@ -81,7 +82,6 @@ import AudioSettingsButton from './AudioSettingsButton';
 import FullscreenButton from './FullscreenButton';
 import OverflowMenuButton from './OverflowMenuButton';
 import ProfileButton from './ProfileButton';
-import RaiseHandButton from './RaiseHandButton';
 import Separator from './Separator';
 import ShareDesktopButton from './ShareDesktopButton';
 import VideoSettingsButton from './VideoSettingsButton';
@@ -256,7 +256,6 @@ class Toolbox extends Component<Props> {
         this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
         this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
-        this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
         this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
         this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
         this._onEscKey = this._onEscKey.bind(this);
@@ -547,8 +546,7 @@ class Toolbox extends Component<Props> {
 
         const raisehand = {
             key: 'raisehand',
-            Content: RaiseHandButton,
-            handleClick: this._onToolbarToggleRaiseHand,
+            Content: ReactionsMenuButton,
             group: 2
         };
 
@@ -1024,23 +1022,6 @@ class Toolbox extends Component<Props> {
         this._doToggleFullScreen();
     }
 
-    _onToolbarToggleRaiseHand: () => void;
-
-    /**
-     * Creates an analytics toolbar event and dispatches an action for toggling
-     * raise hand.
-     *
-     * @private
-     * @returns {void}
-     */
-    _onToolbarToggleRaiseHand() {
-        sendAnalytics(createToolbarEvent(
-            'raise.hand',
-            { enable: !this.props._raisedHand }));
-
-        this._doToggleRaiseHand();
-    }
-
     _onToolbarToggleScreenshare: () => void;
 
     /**
@@ -1144,7 +1125,10 @@ class Toolbox extends Component<Props> {
                                 ariaControls = 'overflow-menu'
                                 isOpen = { _overflowMenuVisible }
                                 key = 'overflow-menu'
-                                onVisibilityChange = { this._onSetOverflowVisible }>
+                                onVisibilityChange = { this._onSetOverflowVisible }
+                                showMobileReactions = {
+                                    overflowMenuButtons.find(({ key }) => key === 'raisehand')
+                                }>
                                 <ul
                                     aria-label = { t(toolbarAccLabel) }
                                     className = 'overflow-menu'
@@ -1154,15 +1138,15 @@ class Toolbox extends Component<Props> {
                                     {overflowMenuButtons.map(({ group, key, Content, ...rest }, index, arr) => {
                                         const showSeparator = index > 0 && arr[index - 1].group !== group;
 
-                                        return (
-                                            <>
+                                        return key !== 'raisehand'
+                                            && <>
                                                 {showSeparator && <Separator key = { `hr${group}` } />}
                                                 <Content
                                                     { ...rest }
                                                     key = { key }
                                                     showLabel = { true } />
                                             </>
-                                        );
+                                        ;
                                     })}
                                 </ul>
                             </OverflowMenuButton>
diff --git a/react/features/toolbox/middleware.js b/react/features/toolbox/middleware.js
index d0c92c0bb..e43cb7b97 100644
--- a/react/features/toolbox/middleware.js
+++ b/react/features/toolbox/middleware.js
@@ -8,6 +8,7 @@ import {
     SET_FULL_SCREEN
 } from './actionTypes';
 
+
 declare var APP: Object;
 
 /**
@@ -18,6 +19,7 @@ declare var APP: Object;
  * @returns {Function}
  */
 MiddlewareRegistry.register(store => next => action => {
+
     switch (action.type) {
     case CLEAR_TOOLBOX_TIMEOUT: {
         const { timeoutID } = store.getState()['features/toolbox'];