Rebuilding Twitter Emoji Finder UI

Don’t get me wrong, I love Twitter’s Emoji search UI. As a coder, I simply enjoy thinking about how something can be improved. So there isn’t in particular anything wrong with it.

First I want to show you finished version of Emoji finder I created. Then we’ll go over the problems that were solved. Is it perfect? Of course not. It might not be even better. But definitely different and has few advantages.

Here is an emoji finder UI I created in vanilla JavaScript with some CSS.

Vanilla implementation of Emoji Finder UI.

Twitter UI is great, however…few theoretical problems exist:

  1. Hard to navigate between large groups of Emojis. Sometimes clicking on an emoji group tab doesn’t help me navigate to the Emoji I am thinking of — I still have to do lots of browsing just to find it once in a filter category.
  2. Words don’t always match proper Emojis. Increasing vocabulary would be definitely one improvement. I type “stars” and single star emojis disappear because plural/singular search is too strict and not thorough.
  3. Color coding. If I type “yellow,” I want to see an entire list of all yellow emojis. I want to see stars, yellow cars, lemon, etc. If you type “blue,” not all blue items will appear in the filtered search.

Solving these issues isn’t difficult from technical point of view. I only found to be slightly difficult to map 3200 emojis to actual JavaScript object and events in terms of how much time it took. Once mapping is done it’s a breeze.

Emoji Sprite Sheet With 3200 Items

There are 3200 emojis. To avoid making thousands of HTTP request all of them are stored in a single file called a sprite sheet or image atlas. Here is a partial view of the official Twitter emoji sprite sheet:

The actual official Twitter emoji sprite sheet image is even bigger than this.

The idea behind a sprite sheet is to store all emoji’s in one file…thus needing only one HTTP request from the server instead of hundreds. And in this case thousands because there are 3200 Twitter emojis as of March 17, 2020.

Each individual emoji is then displayed in a single DIV element and navigated to by changing this property:

background-position: x y;

By supplying this property with an x and y offset in pixels we can navigate to any Emoji in the set. We just need to memorize the position of each emoji and store it in a JavaScript object.

Not the most fun type of work…but you only have to do it once.

Emoji Tabs

I started with rebuilding the emoji tabs interface. I simply didn’t agree with some icon choices made and the number of categories. So I increased the number of available tabs. There are more specifically meaningful subject groups than the 9 tabs Twitter offers.. Nature, Cars, Airplanes, Trains…. it could be more specific.

One thing of importance here is that I added custom attributes data-type (holding the tab type name which will be later used to filter emojis) and data-title which simply stores a simple description of each set.

<div id = "emoji-root"><div class = "tabs"><!-- recent -->
<div class = "tab recent"
data-type = "recent"
data-title = "Recent"></div>
<!-- positive -->
<div class = "tab positive"
data-type = "positive"
data-title = "Positive, Smiley, Smileys"></div>
<!-- negative-->
<div class = "tab negative"
data-type = "negative"
data-title = "Negative, Smiley, Smileys"></div>
<!-- ...all other tabs... --><!-- filtered results box -->
<div id = "emoji-results"></div>
<!-- selected emojis -->
<div id = "emoji-output"></div>
</div>

It’s best to spend more time browsing better categories than hard-to-find emojis. If we don’t merely increase the number of tabs…we can save search time if the tabs themselves are categorized in a more meaningful way.

I separated positive and negative emojis. How many times you try to pick from the positive set? It’s hard to draw the line between the two distinct types when all emotions are shown at once…and yet it is a very useful and practical distinction.

My insert_all_emoji function is called once after DOM is finished loading.

window.addEventListener('DOMContentLoaded', (event) => {
insert_all_emoji();
});

It is responsible for physically creating each emoji in the main search results view. When it’s executed for the first time in application initialization time…it dynamically creates each emoji as a <div> element with display: inline-block. This means each emoji will automatically “wrap” over to the next line within its parent container.

function insert_all_emoji() {
let emojis = 3200;
let max_per_row = 50;
let c = 0;
let y = 0;
let root = document.getElementById("emoji-results");
// Walk through all 3,200 emojis and physically create them
for (let i = 0; i < emojis; i++, c++) {
let E = document.createElement("div");
E.style.width = "48px";
E.style.height = "48px";
E.style.position = "relative";
E.style.display = "inline-block";
E.style.border = "0";
E.style.margin = "4px";
E.style.cursor = "pointer";
E.setAttribute("id", "emoji_id_" + i);
E.setAttribute("class", "emoji");
let x = i * -48;
if (c > max_per_row) { c = 0; y -= 48; }
E.style.backgroundPositionX = x + 'px';
E.style.backgroundPositionY = y + 'px';
// When this emoji is clicked,
// clone it and copy to send message box

E.addEventListener("click", event => {
let clone = event.target.cloneNode();
let what = "#emoji-output";
document.querySelector(what).appendChild(clone);
});
root.appendChild(E);
}
}

Mapping Unique IDs To Background-Position and Emoji Size

Each emoji has a unique numeric ID ranging from 0 to 3199 (since there are 3200 emojis total.) Also important to note…originally all emojis are set to be visible by default. When tabs are clicked, I use another function to “filter” out unwanted emojis from that category.

Emoji position is calculated using CSS’s background-position property. This can be pure nightmare with the set as large as 3200 emojis. So I had to create another script I called Emoji Finder. It basically shows the entire set as an image. Clicking on it with mouse will yield x and y background offset to that emoji in developer’s console. Once I got those results, I brute-force copied them over into my CSS.

Knowing that each emoji is 48px by 48px it’s relatively easy to map into the right background position with just simple math.

Creating Clickable Tabs

In order to create each tab, I simply clicked on the emoji directly on the image and the script gave me accurate offset to it on the entire sprite sheet. I then hard-coded it into my tabs. It was a painful process, but the results speak for themselves. Besides, what would be a better or faster way of doing that? I got it all favorite emojis mapped in a matter of 15 minutes.

Here is the CSS I ended up with:

.tab.recent { background-position: 0 -1536px !important; }
.tab.positive { background-position: -1728px -1680px !important; }
.tab.negative { background-position: -1680px -1680px !important; }
.tab.gestures { background-position: -96px -720px !important; }
.tab.relationship { background-position: -1104px -1344px !important}
.tab.things { background-position: -1056px -1440px !important; }
.tab.nature { background-position: -1488px -288px !important; }
.tab.flowers { background-position: -1728px -288px !important; }
.tab.heavenly { background-position: -192px -288px !important; }
.tab.food { background-position: -672px -336px !important; }
.tab.sports { background-position: -288px -2928px !important; }
.tab.cats { background-position: -1392px -624px !important; }
.tab.animals { background-position: -1392px -624px !important; }
.tab.identity { background-position: -1920px -864px !important; }
.tab.tech { background-position: -2160px -1584px !important; }
.tab.family { background-position: -384px -912px !important; }
.tab.fruit { background-position: -432px -336px !important; }
.tab.vegetable { background-position: -480px -2256px !important; }
.tab.cars { background-position: -1728px -1824px !important; }
.tab.airplanes { background-position: -144px -2976px !important; }
.tab.trains { background-position: -720px -1824px !important; }
.tab.water { background-position: -2016px -240px !important; }
.tab.buildings { background-position: -720px -576px !important; }
.tab.travel { background-position: 0 0 !important; }
.tab.objects { background-position: 0 0 !important; }
.tab.symbols { background-position: 0 0 !important; }
.tab.flags { background-position: -912px -576px !important; }
.tabs.japanese { background-position: -912px -3024px !important; }

It looks a bit convoluted, but there really isn’t much else you can do in order to achieve this functionality. I usually try to walk the extra mile on issues that I know once they are done I never have to touch them again. Fair enough.

Why I love JavaScript higher-order functions.

Higher-order functions are a lifesaver for situations where you have to deal with many items. I can’t imagine writing for-loops for cases like this. HO functions also make my code a lot cleaner and intuitive by means of abstraction!

Remember how in the previous step an entire set of emojis was embedded into the results view by default after DOM finished loading?

Now we need to filter that set. Guess what, I’ll use a higher-order function called .map. I could have probably used .filter. In this particular case there is no difference.

filter_emojis is my favorite function from this entire project. It was lots of fun to write. Usually when there is a concrete problem to solve, coding becomes a lot more rewarding. And this was one of those cases!

// Filter emojis by category
function filter_emojis(type) {
let positive = [308, 311, 312, 969, 970, 1018, 1019, 1028, 1036, 1051, 1352, 1747, 1748, 1770, 1771, 1772, 1773, 1774, 1775, 1776, 1777, 1778, 1779, 1780, 1781, 1782, 1783, 1784, 1786, 1787, 1830, 1826, 1827, 1828, 2164, 2208, 2209, 2211, 2212, 2234, 2235, 2437, 2439, 2769, 2770, 2778, 3106];let negative = [679, 814, 1785, 1788, 1800, 1801, 1802, 1803, 1804, 1805, 1806, 1807, 1808, 1809, 1810, 1811, 1812, 1813, 1814, 1815, 1816, 1817, 1818, 1819, 1820, 1821, 1822, 1823, 1824, 1825, 1830, 1832, 1833, 1834, 1835, 2156, 2157, 2159, 2161, 2162, 2210, 2213, 2232, 2233, 2236, 2237, 2239, 2440, 2441, 2442, 2443, 2618, 3000, 3001, 3002, 3065];let gestures = [716, 722, 728, 734, 740, 746, 752, 758, 770, 776, 802, 808, 814, 1445, 1687, 1693, 1731, 1789, 1834, 1842, 1843, 1844, 1845, 1846, 1849, 1862, 1874, 1887, 1900, 1901, 1911, 1912, 1913, 1961, 2144, 2145, 2156, 2157, 2161, 2164, 2170, 2176, 2182, 2188, 2201, 2207, 2208, 2213, 2217, 2218, 2219, 2229, 2230, 2231, 2234, 2236, 2238, 2258, 2438, 2448, 2498, 2504, 2548, 2551, 2552, 3166, 3172, 3178];let relationship = [409, 421, 422, 1428, 1429, 1454, 1455, 1458, 1459, 1463, 1465, 1466, 1467, 1468, 1469, 1470, 1471, 1472, 1473, 1474, 1475, 1476, 1477, 1478, 1617, 1672, 1744, 1745, 1743, 1746, 1780, 1783, 1829, 1831, 2150, 2437, 3084, 3067, 3068, 3100, 3196, 3197, 3185, 3150, 2087, 2088, 2089, 2090, 362, 340, 339, 338, 337, 336, 335, 294];let cats = [629, 630, 634, 635, 679, 687, 692, 700, 701, 1826, 1827, 1828, 1829, 1830, 1831, 1832, 1833, 1834, 2450];let things = [284, 409, 444, 445, 435, 448, 449, 454, 456, 457, 470, 472, 504, 507, 508, 516, 517, 524, 583, 586, 626, 628, 777, 778, 779, 781, 782, 783, 784, 785, 786, 788, 789, 791, 792, 1408, 1433, 1431, 1464, 1480, 1481, 1482, 1492, 1493, 1485, 1499, 1501, 1512, 1513, 1514, 1515, 1516, 1517, 1529, 1541, 1548, 1549, 1550, 1551, 1552, 1564, 1572, 1573, 1574, 1575, 1576, 1577, 1578, 1618, 1619, 1620, 1621, 1622, 1623, 1624, 1625, 1626, 1672, 1673, 1695, 1696, 1697, 1698, 1711, 1714, 1722, 1723, 1724, 1725, 1750, 1751, 1752, 1753, 1754, 1755, 1756, 1757, 1758, 1759, 1764, 2117, 2119, 2135, 2128, 2389, 2391, 2393, 2394, 2396, 2397, 2398, 2400, 2401, 2408, 2409, 2410, 2406, 2416, 2415, 2426, 2427, 2428, 2446, 2553, 2555, 2557, 2958, 2959, 2961, 2963, 2964, 2966, 2967, 2968, 2969, 2970, 2971, 2972, 2973, 2974, 2975, 2976, 2978, 2980, 2982, 2988, 2991, 2996, 2997, 2998, 2999, 3005, 3006, 3007, 3008, 3014, 3015, 3016, 3025, 3026, 3027, 3028, 3043, 3046, 3048, 3049, 3090, 3091, 3092, 3094, 3095, 3096, 3107, 3112, 3130, 3154, 3155, 3156, 3157, 3179, 3180, 3185];let trees = [329, 330, 331, 332, 333, 342, 343, 344, 345, 346, 347, 348, 1495, 3100];let flowers = [329, 332, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 1464, 3100];let heavenly = [308, 309, 310, 311, 312, 313, 314, 316, 317, 318, 319, 320, 321, 306, 307, 294, 286, 287, 288, 289, 290, 291, 350, 351, 352, 353, 354, 355, 356, 1437, 1625, 1626, 3041, 3159, 3160, 3185, 3150];let water = [318, 319, 320, 513, 526, 527, 542, 543, 681, 674, 711, 712, 713, 686, 1436, 1437, 1764, 2015, 2043, 2027, 2028, 2124, 2132, 2129, 2369, 2370, 2389, 2425, 3160, 3188, 3122, 3123, 3095, 2926, 2927, 2928, 292];let food = [349, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 410, 450, 451, 452, 453, 454, 455, 456, 457, 2406, 2405, 2407, 2408, 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, 2419, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 3049, 3086];let all = { "positive" : positive,
"negative" : negative,
"gestures" : gestures,
"relationship" : relationship,
"cats" : cats,
"things" : things,
"trees" : trees,
"flowers" : flowers,
"heavenly" : heavenly,
"water" : water,
"food" : food,};
// get clicked tab's type
let selected = all[type];
// walk through each .emoji class and hide all
let
classes = document.querySelectorAll(".emoji");
classes.forEach(emoji => { emoji.style.display = "none"; });
// display only search-filtered emojis
selected.map(item => {
let em = document.getElementById("emoji_id_" + item);
em.style.display = "inline-block";
});
}

But how does filter function know which emojis to keep and which to remove from the set? All IDs are stored in individual arrays representing those emojis.

I created a script that when I click on any emoji in the main view, it adds its unique id to an array. Going around entire list and clicking on emojis I was able to create lists I thought were associated with any particular tab. One by one, I created each list by simply clicking on emojis. The IDs that were generated were placed into arrays seen above (positive, negative, gestures, relationship, cats, etc.)

What happens now is first I set display to none for the entire set. Then, I use higher-order .map function to set display back to inline-block but only to all emojis contained with selected set.

The onclick event to this function is tied to each tab using this function:

// Attach onclick event listeners to emoji category tabs
function attach_tab_filters() {
document.querySelectorAll(".tab").forEach(item =>
item.addEventListener("click", event =>
filter_emojis(item.getAttribute("data-type"))));
}

But What About Search?

That’s everything for now. I need for everything I created during last two days to sink in for a while. Then I’ll add search as part II of this tutorial.

Then, next time I update this tutorial, I plan on writing an emoji search function. This will take some time…I have to write a list of keywords describing 3200 emojis with more keywords than Twitter.

This is a necessary step…and will definitely an improvement! For example, I noticed Twitter Emoji search is broken in some places. There is no color coding. If you type “blue” you won’t get absolutely all blue objects in the set. But IMO color coding is so important for something like this.

Thanks for reading! Hope this tutorial was helpful in some way 😊

Written by

Issues. Every webdev has them. Published author of CSS Visual Dictionary https://amzn.to/2JMWQP3 few others…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store