diff --git a/web/chat/chat-constants.js b/web/chat/chat-constants.js
--- a/web/chat/chat-constants.js
+++ b/web/chat/chat-constants.js
@@ -16,3 +16,12 @@
   width: 30,
   height: 38,
 };
+
+export const typeaheadStyle = {
+  tooltipWidth: 296,
+  tooltipMaxHeight: 268,
+  tooltipVerticalPadding: 16,
+  tooltipLeftOffset: 16,
+  tooltipTopOffset: 4,
+  rowHeight: 40,
+};
diff --git a/web/chat/mention-utils.js b/web/chat/mention-utils.js
--- a/web/chat/mention-utils.js
+++ b/web/chat/mention-utils.js
@@ -1,5 +1,7 @@
 // @flow
 
+import * as React from 'react';
+
 import { oldValidUsernameRegexString } from 'lib/shared/account-utils';
 import SearchIndex from 'lib/shared/search-index';
 import { stringForUserExplicit } from 'lib/shared/user-utils';
@@ -9,6 +11,20 @@
   `(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`,
 );
 
+import { type InputState } from '../input/input-state';
+import { typeaheadStyle } from './chat-constants';
+
+export type MentionSuggestionTooltipAction = {
+  +key: string,
+  +onClick: (SyntheticEvent<HTMLButtonElement>) => mixed,
+  +actionButtonContent: React.Node,
+};
+
+export type TooltipPosition = {
+  +top: number,
+  +left: number,
+};
+
 function getTypeaheadUserSuggestions(
   userSearchIndex: SearchIndex,
   usersInThread: $ReadOnlyArray<RelativeMemberInfo>,
@@ -24,7 +40,7 @@
 }
 
 function getCaretOffsets(
-  textarea: ?HTMLTextAreaElement,
+  textarea: HTMLTextAreaElement,
   text: string,
 ): { caretTopOffset: number, caretLeftOffset: number } {
   if (!textarea) {
@@ -58,10 +74,86 @@
   const { offsetTop, offsetLeft } = span;
   document.body?.removeChild(div);
 
+  const textareaWidth = parseInt(textareaStyle.getPropertyValue('width'));
+
+  const caretLeftOffset =
+    offsetLeft + typeaheadStyle.tooltipWidth > textareaWidth
+      ? textareaWidth - typeaheadStyle.tooltipWidth
+      : offsetLeft;
+
   return {
     caretTopOffset: offsetTop - textarea.scrollTop,
-    caretLeftOffset: offsetLeft,
+    caretLeftOffset,
   };
 }
 
-export { mentionRegex, getTypeaheadUserSuggestions, getCaretOffsets };
+function getTypeaheadTooltipActions(
+  inputState: InputState,
+  textarea: HTMLTextAreaElement,
+  suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
+  matchedTextBefore: string,
+  wholeMatch: string,
+): $ReadOnlyArray<MentionSuggestionTooltipAction> {
+  return suggestedUsers
+    .filter(
+      suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous',
+    )
+    .map(suggestedUser => ({
+      key: suggestedUser.id,
+      onClick: () => {
+        const newPrefixText = matchedTextBefore;
+
+        let newSuffixText = inputState.draft.slice(wholeMatch.length);
+        newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText;
+
+        const newText =
+          newPrefixText +
+          '@' +
+          stringForUserExplicit(suggestedUser) +
+          newSuffixText;
+
+        inputState.setDraft(newText);
+        inputState.setTextCursorPosition(
+          newText.length - newSuffixText.length + 1,
+        );
+      },
+      actionButtonContent: stringForUserExplicit(suggestedUser),
+    }));
+}
+function getTypeaheadTooltipPosition(
+  textarea: HTMLTextAreaElement,
+  actionsLength: number,
+  matchedTextBefore: string,
+): TooltipPosition {
+  const { caretTopOffset, caretLeftOffset } = getCaretOffsets(
+    textarea,
+    matchedTextBefore,
+  );
+
+  const textareaBoundingClientRect = textarea.getBoundingClientRect();
+
+  const top: number =
+    textareaBoundingClientRect.top -
+    Math.min(
+      typeaheadStyle.tooltipVerticalPadding +
+        actionsLength * typeaheadStyle.rowHeight,
+      typeaheadStyle.tooltipMaxHeight,
+    ) -
+    typeaheadStyle.tooltipTopOffset +
+    caretTopOffset;
+
+  const left: number =
+    textareaBoundingClientRect.left -
+    typeaheadStyle.tooltipLeftOffset +
+    caretLeftOffset;
+
+  return { top, left };
+}
+
+export {
+  mentionRegex,
+  getTypeaheadUserSuggestions,
+  getCaretOffsets,
+  getTypeaheadTooltipActions,
+  getTypeaheadTooltipPosition,
+};