diff --git a/web/chat/thread-menu.css b/web/chat/thread-menu.css
--- a/web/chat/thread-menu.css
+++ b/web/chat/thread-menu.css
@@ -1,58 +1,6 @@
-button.topBarMenuButton {
-  background-color: transparent;
-  border: none;
-  cursor: pointer;
-  color: var(--thread-top-bar-menu-color);
-}
-
-div.topBarMenuActionList {
-  position: absolute;
-  right: 10px;
-  top: 55px;
-  z-index: 1;
-  display: flex;
-  flex-direction: column;
-  background-color: var(--thread-menu-bg);
-  border-radius: 4px;
-  padding: 4px 0;
-}
-
-button.topBarMenuAction {
-  z-index: 1;
-  background-color: transparent;
-  padding: 12px 16px;
-  color: var(--thread-menu-color);
-  background-color: var(--thread-menu-bg);
-  font-size: var(--m-font-16);
-  line-height: 1.5;
-  border: none;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-}
-
-button.topBarMenuAction:hover {
-  color: var(--thread-menu-color-hover);
-}
-
-div.topBarMenuActionIcon {
-  font-size: var(--l-font-18);
-  display: flex;
-  justify-content: center;
-  margin-right: 8px;
-  width: 20px;
-}
-
-button.topBarMenuActionDangerous {
-  color: var(--thread-menu-color-dangerous);
-}
-button.topBarMenuActionDangerous:hover {
-  color: var(--thread-menu-color-dangerous-hover);
-}
-
 hr.separator {
   height: 1px;
-  background: var(--thread-menu-separator-color);
+  background: var(--menu-separator-color);
   margin: 10px 16px;
   max-width: 130px;
   border: none;
diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js
--- a/web/chat/thread-menu.react.js
+++ b/web/chat/thread-menu.react.js
@@ -31,13 +31,14 @@
   useDispatchActionPromise,
 } from 'lib/utils/action-utils';
 
+import MenuItem from '../components/menu-item.react';
+import Menu from '../components/menu.react';
 import SidebarListModal from '../modals/chat/sidebar-list-modal.react';
 import { useModalContext } from '../modals/modal-provider.react';
 import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react';
 import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react';
 import { useSelector } from '../redux/redux-utils';
 import SWMansionIcon from '../SWMansionIcon.react';
-import ThreadMenuItem from './thread-menu-item.react';
 import css from './thread-menu.css';
 
 type ThreadMenuProps = {
@@ -45,8 +46,6 @@
 };
 
 function ThreadMenu(props: ThreadMenuProps): React.Node {
-  const [isOpen, setIsOpen] = React.useState(false);
-
   const { setModal, clearModal } = useModalContext();
 
   const { threadInfo } = props;
@@ -58,7 +57,7 @@
 
   const settingsItem = React.useMemo(() => {
     return (
-      <ThreadMenuItem
+      <MenuItem
         key="settings"
         text="Settings"
         icon={faCog}
@@ -71,7 +70,7 @@
     if (threadInfo.type === threadTypes.PERSONAL) {
       return null;
     }
-    return <ThreadMenuItem key="members" text="Members" icon={faUserFriends} />;
+    return <MenuItem key="members" text="Members" icon={faUserFriends} />;
   }, [threadInfo.type]);
 
   const childThreads = useSelector(
@@ -94,7 +93,7 @@
       return null;
     }
     return (
-      <ThreadMenuItem
+      <MenuItem
         key="sidebars"
         text="Sidebars"
         icon={faArrowRight}
@@ -117,11 +116,7 @@
       return null;
     }
     return (
-      <ThreadMenuItem
-        key="subchannels"
-        text="Subchannels"
-        icon={faCommentAlt}
-      />
+      <MenuItem key="subchannels" text="Subchannels" icon={faCommentAlt} />
     );
   }, [canCreateSubchannels, hasSubchannels]);
 
@@ -130,7 +125,7 @@
       return null;
     }
     return (
-      <ThreadMenuItem
+      <MenuItem
         key="newSubchannel"
         text="Create new subchannel"
         icon={faPlusCircle}
@@ -170,7 +165,7 @@
       return null;
     }
     return (
-      <ThreadMenuItem
+      <MenuItem
         key="leave"
         text="Leave Thread"
         icon={faSignOutAlt}
@@ -182,7 +177,7 @@
 
   const menuItems = React.useMemo(() => {
     const notificationsItem = (
-      <ThreadMenuItem key="notifications" text="Notifications" icon={faBell} />
+      <MenuItem key="notifications" text="Notifications" icon={faBell} />
     );
     const separator = <hr key="separator" className={css.separator} />;
 
@@ -211,45 +206,11 @@
     createSubchannelsItem,
     leaveThreadItem,
   ]);
-
-  const closeMenuCallback = React.useCallback(() => {
-    document.removeEventListener('click', closeMenuCallback);
-    if (isOpen) {
-      setIsOpen(false);
-    }
-  }, [isOpen]);
-
-  React.useEffect(() => {
-    if (!document || !isOpen) {
-      return undefined;
-    }
-    document.addEventListener('click', closeMenuCallback);
-    return () => document.removeEventListener('click', closeMenuCallback);
-  }, [closeMenuCallback, isOpen]);
-
-  const switchMenuCallback = React.useCallback(() => {
-    setIsOpen(isMenuOpen => !isMenuOpen);
-  }, []);
-
-  if (menuItems.length === 0) {
-    return null;
-  }
-
-  let menuActionList = null;
-  if (isOpen) {
-    menuActionList = (
-      <div className={css.topBarMenuActionList}>{menuItems}</div>
-    );
-  }
-
-  return (
-    <>
-      <button className={css.topBarMenuButton} onClick={switchMenuCallback}>
-        <SWMansionIcon icon="menu-vertical" size={24} />
-      </button>
-      {menuActionList}
-    </>
+  const icon = React.useMemo(
+    () => <SWMansionIcon icon="menu-vertical" size={20} />,
+    [],
   );
+  return <Menu icon={icon}>{menuItems}</Menu>;
 }
 
 export default ThreadMenu;
diff --git a/web/chat/thread-menu-item.react.js b/web/components/menu-item.react.js
rename from web/chat/thread-menu-item.react.js
rename to web/components/menu-item.react.js
--- a/web/chat/thread-menu-item.react.js
+++ b/web/components/menu-item.react.js
@@ -5,24 +5,24 @@
 import classNames from 'classnames';
 import * as React from 'react';
 
-import css from './thread-menu.css';
+import css from './menu.css';
 
-type ThreadMenuItemProps = {
+type MenuItemProps = {
   +onClick?: () => mixed,
   +icon: IconDefinition,
   +text: string,
   +dangerous?: boolean,
 };
 
-function ThreadMenuItem(props: ThreadMenuItemProps): React.Node {
+function MenuItem(props: MenuItemProps): React.Node {
   const { onClick, icon, text, dangerous } = props;
 
-  const itemClasses = classNames(css.topBarMenuAction, {
-    [css.topBarMenuActionDangerous]: dangerous,
+  const itemClasses = classNames(css.menuAction, {
+    [css.menuActionDangerous]: dangerous,
   });
   return (
     <button className={itemClasses} onClick={onClick}>
-      <div className={css.topBarMenuActionIcon}>
+      <div className={css.menuActionIcon}>
         <FontAwesomeIcon icon={icon} className={css.promptIcon} />
       </div>
       <div>{text}</div>
@@ -30,8 +30,8 @@
   );
 }
 
-const MemoizedThreadMenuItem: React.ComponentType<ThreadMenuItemProps> = React.memo(
-  ThreadMenuItem,
+const MemoizedMenuItem: React.ComponentType<MenuItemProps> = React.memo(
+  MenuItem,
 );
 
-export default MemoizedThreadMenuItem;
+export default MemoizedMenuItem;
diff --git a/web/chat/thread-menu.css b/web/components/menu.css
copy from web/chat/thread-menu.css
copy to web/components/menu.css
--- a/web/chat/thread-menu.css
+++ b/web/components/menu.css
@@ -1,28 +1,28 @@
-button.topBarMenuButton {
+button.menuButton {
   background-color: transparent;
   border: none;
   cursor: pointer;
   color: var(--thread-top-bar-menu-color);
 }
 
-div.topBarMenuActionList {
+div.menuActionList {
   position: absolute;
   right: 10px;
   top: 55px;
   z-index: 1;
   display: flex;
   flex-direction: column;
-  background-color: var(--thread-menu-bg);
+  background-color: var(--menu-bg);
   border-radius: 4px;
   padding: 4px 0;
 }
 
-button.topBarMenuAction {
+button.menuAction {
   z-index: 1;
   background-color: transparent;
   padding: 12px 16px;
-  color: var(--thread-menu-color);
-  background-color: var(--thread-menu-bg);
+  color: var(--menu-color);
+  background-color: var(--menu-bg);
   font-size: var(--m-font-16);
   line-height: 1.5;
   border: none;
@@ -31,11 +31,11 @@
   align-items: center;
 }
 
-button.topBarMenuAction:hover {
-  color: var(--thread-menu-color-hover);
+button.menuAction:hover {
+  color: var(--menu-color-hover);
 }
 
-div.topBarMenuActionIcon {
+div.menuActionIcon {
   font-size: var(--l-font-18);
   display: flex;
   justify-content: center;
@@ -43,16 +43,16 @@
   width: 20px;
 }
 
-button.topBarMenuActionDangerous {
-  color: var(--thread-menu-color-dangerous);
+button.menuActionDangerous {
+  color: var(--menu-color-dangerous);
 }
-button.topBarMenuActionDangerous:hover {
-  color: var(--thread-menu-color-dangerous-hover);
+button.menuActionDangerous:hover {
+  color: var(--menu-color-dangerous-hover);
 }
 
 hr.separator {
   height: 1px;
-  background: var(--thread-menu-separator-color);
+  background: var(--menu-separator-color);
   margin: 10px 16px;
   max-width: 130px;
   border: none;
diff --git a/web/components/menu.react.js b/web/components/menu.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/menu.react.js
@@ -0,0 +1,55 @@
+// @flow
+
+import * as React from 'react';
+
+import css from './menu.css';
+
+type MenuProps = {
+  +icon: React.Node,
+  +children?: React.Node,
+};
+
+function Menu(props: MenuProps): React.Node {
+  const [isOpen, setIsOpen] = React.useState(false);
+
+  const { icon, children } = props;
+
+  const closeMenuCallback = React.useCallback(() => {
+    document.removeEventListener('click', closeMenuCallback);
+    if (isOpen) {
+      setIsOpen(false);
+    }
+  }, [isOpen]);
+
+  React.useEffect(() => {
+    if (!document || !isOpen) {
+      return undefined;
+    }
+    document.addEventListener('click', closeMenuCallback);
+    return () => document.removeEventListener('click', closeMenuCallback);
+  }, [closeMenuCallback, isOpen]);
+
+  const switchMenuCallback = React.useCallback(() => {
+    setIsOpen(isMenuOpen => !isMenuOpen);
+  }, []);
+
+  if (React.Children.count(children) === 0) {
+    return null;
+  }
+
+  let menuActionList = null;
+  if (isOpen) {
+    menuActionList = <div className={css.menuActionList}>{children}</div>;
+  }
+
+  return (
+    <div>
+      <button className={css.menuButton} onClick={switchMenuCallback}>
+        {icon}
+      </button>
+      {menuActionList}
+    </div>
+  );
+}
+
+export default Menu;
diff --git a/web/theme.css b/web/theme.css
--- a/web/theme.css
+++ b/web/theme.css
@@ -104,12 +104,12 @@
   --thread-ancestor-separator-color: var(--shades-white-60);
   --text-message-default-background: var(--shades-black-80);
   --message-action-tooltip-bg: var(--shades-black-90);
-  --thread-menu-bg: var(--shades-black-90);
-  --thread-menu-separator-color: var(--shades-black-80);
-  --thread-menu-color: var(--shades-black-60);
-  --thread-menu-color-hover: var(--shades-white-100);
-  --thread-menu-color-dangerous: var(--error-primary);
-  --thread-menu-color-dangerous-hover: var(--error-light-50);
+  --menu-bg: var(--shades-black-90);
+  --menu-separator-color: var(--shades-black-80);
+  --menu-color: var(--shades-black-60);
+  --menu-color-hover: var(--shades-white-100);
+  --menu-color-dangerous: var(--error-primary);
+  --menu-color-dangerous-hover: var(--error-light-50);
   --app-list-icon-read-only-color: var(--shades-black-60);
   --app-list-icon-enabled-color: var(--success-primary);
   --app-list-icon-disabled-color: var(--shades-white-80);