diff --git a/web/chat/typeahead-tooltip.react.js b/web/chat/typeahead-tooltip.react.js
--- a/web/chat/typeahead-tooltip.react.js
+++ b/web/chat/typeahead-tooltip.react.js
@@ -4,6 +4,7 @@
 import * as React from 'react';
 
 import type { RelativeMemberInfo } from 'lib/types/thread-types';
+import { leastPositiveResidue } from 'lib/utils/math-utils';
 
 import type { InputState } from '../input/input-state';
 import {
@@ -30,6 +31,15 @@
     false,
   );
 
+  const [
+    chosenPositionInOverlay,
+    setChosenPositionInOverlay,
+  ] = React.useState<number>(0);
+
+  React.useEffect(() => {
+    setChosenPositionInOverlay(0);
+  }, [suggestedUsers]);
+
   React.useEffect(() => {
     setIsVisibleForAnimation(true);
 
@@ -75,7 +85,61 @@
     [actions],
   );
 
-  if (!actions || actions.length === 0) {
+  const close = React.useCallback(() => {
+    const setter = inputState.setTypeaheadState;
+    setter({
+      canBeVisible: false,
+      moveChoiceUp: null,
+      moveChoiceDown: null,
+      close: null,
+      accept: null,
+    });
+  }, [inputState.setTypeaheadState]);
+
+  const accept = React.useCallback(() => {
+    actions[chosenPositionInOverlay].execute();
+    close();
+  }, [actions, chosenPositionInOverlay, close]);
+
+  const moveChoiceUp = React.useCallback(() => {
+    if (actions.length === 0) {
+      return;
+    }
+    setChosenPositionInOverlay(previousPosition =>
+      leastPositiveResidue(previousPosition - 1, actions.length),
+    );
+  }, [setChosenPositionInOverlay, actions.length]);
+
+  const moveChoiceDown = React.useCallback(() => {
+    if (actions.length === 0) {
+      return;
+    }
+    setChosenPositionInOverlay(previousPosition =>
+      leastPositiveResidue(previousPosition + 1, actions.length),
+    );
+  }, [setChosenPositionInOverlay, actions.length]);
+
+  React.useEffect(() => {
+    const setter = inputState.setTypeaheadState;
+    setter({
+      canBeVisible: true,
+      moveChoiceUp,
+      moveChoiceDown,
+      close,
+      accept,
+    });
+
+    return close;
+  }, [
+    close,
+    accept,
+    moveChoiceUp,
+    moveChoiceDown,
+    actions,
+    inputState.setTypeaheadState,
+  ]);
+
+  if (suggestedUsers.length === 0) {
     return null;
   }
 
diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js
--- a/web/utils/typeahead-utils.js
+++ b/web/utils/typeahead-utils.js
@@ -16,7 +16,7 @@
 
 export type TypeaheadTooltipAction = {
   +key: string,
-  +onClick: (SyntheticEvent<HTMLButtonElement>) => mixed,
+  +execute: () => mixed,
   +actionButtonContent: React.Node,
 };
 
@@ -98,7 +98,7 @@
     )
     .map(suggestedUser => ({
       key: suggestedUser.id,
-      onClick: () => {
+      execute: () => {
         const newPrefixText = textBeforeAtSymbol;
 
         const totalMatchLength =
@@ -125,8 +125,8 @@
 function getTypeaheadTooltipButtons(
   actions: $ReadOnlyArray<TypeaheadTooltipAction>,
 ): $ReadOnlyArray<React.Node> {
-  return actions.map(({ key, onClick, actionButtonContent }) => (
-    <Button key={key} onClick={onClick} className={css.suggestion}>
+  return actions.map(({ key, execute, actionButtonContent }) => (
+    <Button key={key} onClick={execute} className={css.suggestion}>
       <span>@{actionButtonContent}</span>
     </Button>
   ));