Building an Instagram Hashtag Typeahead in JavaScript

Marty Jones
Tailwind
Published in
6 min readApr 18, 2019

At Tailwind, one of our core features is to give Tailwind members suggestions about hashtags to use when crafting a post, so that our members can maximize the reach of their Instagram posts.

One way that we accomplish this is by providing the list of suggested hashtags that the member can select while writing a post caption — You can see this in the image below.

The different colors for the suggested hashtags help the member identify the relevance of each suggested tag.

One major UX improvement that our design and engineering team collaborated on was to be able to make suggestions as soon as the member starts to type a hashtag. If I start typing #foo in the post above, for example, we'd like a list to come up that shows hashtag suggestions including things like #football and #footballgames.

Enabling a typeahead for members will make creating a post easier and faster when the member has to decide which hashtags to use in the post’s caption.

The goal

As the member types, we’d like to suggest hashtags that begin with the letters the member has entered so far. The suggestions should be brought up in context, right below the text that the member is typing so that the member can quickly select one and continue editing the post caption. We also want to provide reach metrics so that the member can choose the best possible tag. Here’s how it will all look when put together:

See it in action at tailwindapp.com

Sounds pretty straightforward, right? Turns out, there are several technical challenges and constraints that we had to consider.

Let’s walk through the main challenges of building and rendering a typeahead.

How to detect whether a hashtag is being created/edited

When a member is in the process of editing an Instagram post caption, one of several different things can be true:

1. The member might be typing a new hashtag or editing an existing one

2. The member may be focused on a hashtag, but not actually editing the hashtag at the moment

3. The member could be editing the post caption but not typing a hashtag

In which cases would we want to display a typeahead?

As it turns out, we would like to display the typeahead for case #1, but not cases #2 and #3. Typeaheads are powerful components, but they should be used only in cases where you know they are helpful to your member, which is why we only want to show our typeahead when the member is typing a hashtag.

To render the typeahead or not to render — That is the question

In order to determine whether the typeahead should be shown, we need to get the hashtag (we’ll call that the activeHashtag) that the member is creating/editing. To do that, we need:

1. A keydown event handler that will fire every time the member presses a key (or a key combination such as shift + a)

The event handler is pretty straightforward. It will call the getActiveHashtag method and return the resulting hashtag that it finds:

// Event handler for when the user presses 
// a key inside of the post editor
onKeyPress = event => {
const content = event.target.value;
const key = event.key;
const caretIndex = event.target.selectionStart;
return getActiveHashtag(content, key, caretIndex)
}

2. A function called getActiveHashtag that takes the following arguments:

  • content - The content of the post caption that the member is editing. E.g. Hello #world!
  • key - The key that the member pressed to trigger this keydown event. We retrieve this from the event object. E.g. if the member presses a key, then event.key would be a. There's more information available as part of the event object that can determine whether a key combination was pressed (such as shift + A), but in our case, event.key is all we care about.
  • caretIndex - In order to determine what part of the post caption the member is editing, we need to know where the caret is. so that we know exactly what part of the post caption the member is editing. E.g. if the member's caret is here: #hello worl| , then the caret index would be 11. We get this information from event.target.selectionStart.

The getActiveHashtag function will either return the hashtag the member is editing (e.g. #worl), or null if no hashtag is actively being edited by the member.

With these arguments in mind and this desired output (either a string that is the hashtag, or null), let's build some pseudocode test cases that we can use to determine whether or not a member is editing the post caption! At Tailwind, we do Test-Driven Development, using tape, to reduce bugs and ensure that we've planned our implementation correctly before we write the actual code.

Case 1 — Member is creating or editing a hashtag:

const test = () => {
// Arrange
const content = 'Hello #worl';
const key = 'l';
const caretIndex = 11; // End of line

// Act
const r = getActiveHashtag(content, key, caretIndex);

// Assert
test.assert(r, `#worl`);
};

Case 2 — Member’s caret is inside of a hashtag but the member is not editing it:

const test = () => {
// Arrange
const content = 'Hello #world';
const key = 'ArrowLeft';
const caretIndex = 11; // Between the `l` and the `d`

// Act
const r = getActiveHashtag(content, key, caretIndex);

// Assert
test.assert(r, null);
};

Case 3 — Member’s caret is not focused on a hashtag:

const test = () => {
// Arrange
const content = '#Hello worl';
const key = 'l';
const caretIndex = 11; // End of line

// Act
const r = getActiveHashtag(content, key, caretIndex);

// Assert
test.assert(r, null);
};

Now that we know what results getActiveHashtag should return, let's look at the implementation:

// Keys that never used in the process of
// editing a hashtag.
const nonEditingKeys = [
'ArrowLeft',
'ArrowRight',
'Control',
'Shift',
// ...etc
];

// Returns the actively-being-edited hashtag, or null
// if none is found.
const getActiveHashtag = (content, key, caretIndex) => {
// If the user pressed a key that isn't a character, they
// are not actively editing a hashtag:
if (nonEditingKeys.includes(key)) {
return null;
}

// Figure out what word or hashtag the user is editing
// using the caret position and the content:
const activeWordOrHashtag = extractActiveWordOrHashtag(content, caretIndex);

// if the word that the user is editing is a hashtag, return it.
// otherwise, return null.
return activeWordOrHashtag[0] === '#' ? activeWordOrHashtag : null;
};

You’ll notice that we have a supporting function, extractActiveWordOrHashtag, that is responsible for getting the word or hashtag that the member is editing from the post caption. Here's how it looks:

// Regex pattern that matches to a word or a hashtag.
// Test it out here: [https://regex101.com/r/0Bl07o/2](https://regex101.com/r/0Bl07o/2)
const hashtagOrWordRegex = /#*\w.*/g;

// Gets the word that the user's caret is positioned on.
const extractActiveWordOrHashtag = (content, caretIndex) => {
// First, backtrack until we find a character that can't
// be part of a word or hashtag.
let index = caretIndex;
let character = content[index];
do {
let matches = char.match(hashtagOrWordRegex);
// if this character is not part of a hashtag (e.g.
// it's a space or a period), return the word or
// hashtag in front of it.
if (!matches || !matches.length) {
return content
.slice(index + 1, content.length)
.match(hashtagOrWordRegex)[0];
}
// Otherwise, go to the previous character
index -= 1
} while (index > 0)
}

Rendering the Typeahead using React

Thanks to our keydown handler and getActiveHashtag, the logic for whether or not to render the typeahead is in place. In our keydown handler, we return the result of getActiveHashtag. If this result isn't null, we know that we need to render the typeahead; so we can pass activeHashtag as a prop to our HashtagTypeahead component and use it in the render method like so:

class HashtagTypeahead extends Component {

...

render () {
// If the user is editing a hashtag,
// render the typeahead to give the
// user suggestions for hashtags to use
if (this.props.activeHashtag) {
return (
<HashtagTypeaheadMenu
activeHashtag={this.props.activeHashtag}
hashtagOptions={...}
onSelect={...)
/>
)
}
// Otherwise, return nothing
return null;
}
}

I abstracted away some of the React implementation details because the way this is done depends on the specific typeahead library being used; picking a typeahead library and implement it could easily be its own blog post! At Tailwind, we use React extensively and have some internally-built components that do typeaheads for us. Some excellent off-the-shelf typeahead components include React Bootstrap Typeahead or React Autosuggest. These might spare you the time and energy of building your own typeahead.

This is my first project since starting at Tailwind and I had a ton of fun working on it. If you’re interested in solving interesting frontend and backend problems, we’re hiring! If you have questions, either about this story or about Tailwind, you can reach me at marty@tailwindapp.com.

Originally published at https://martyjon.es on April 19, 2019.

--

--