Remaking Twitter Emoji Finder UI In Vanilla JS Posted on March 17 2020 by @js_tut First I want to show you the finished version of Emoji search created in this tutorial. Then we'll go over the problems that were solved. Is it perfect? Of course not. It might not be even better. But it's definitely different and has a few advantages. Why Improve Twitter Emoji UI - Independent UI and UX Usability Study Twitter is one of the best apps in the social network arena. As masters of digital conversation they even provide a quick emoji search and picker! But...there is always a but. And it can be improved in many usability areas. Below is shown a partial Twitter emoji sprite sheet. The actual official Twitter emoji sprite sheet image is much 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 propery: 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"> <div class = "tab recent" data-type = "recent" data-title = "Recent"></div> <div class = "tab positive" data-type = "positive" data-title = "Positive, Smiley, Smileys"></div> <div class = "tab negative" data-type = "negative" data-title = "Negative, Smiley, Smileys"></div> <div class = "tab gestures" data-type = "gestures" data-title = "Hand, Gestures"></div> <div class = "tab relationship" data-type = "relationship" data-title = "Heart"></div> <div class = "tab cats" data-type = "cats" data-title = "Animals (Cat, Dog)"></div> <div class = "tab animals" data-type = "animals" data-title = "Animals (Cat, Dog)"></div> <div class = "tab things" data-type = "things" data-title = "Things"></div> <div class = "tab nature" data-type = "nature" data-title = "Nature, Tres"></div> <div class = "tab flowers" data-type = "flowers" data-title = "Flowers, Floral"></div> <div class = "tab heavenly" data-type = "heavenly" data-title = "Weather, Sky, Heavens, Sun, Moon, Celestial"></div> <div class = "tab water" data-type = "water" data-title = "Ocea, Sea, Water"></div> <div class = "tab food" data-type = "food" data-title = "Food"></div> <div class = "tab fruit" data-type = "fruit" data-title = "Fruit"></div> <div class = "tab vegetable" data-type = "vegetable" data-title = "Vegetable"></div> <div class = "tab sports" data-type = "sports" data-title = "Activity (Sports)"></div> <div class = "tab cars" data-type = "cars" data-title = "Cars"></div> <div class = "tab airplanes" data-type = "airplanes" data-title = "Air, Planes, Airplanes"></div> <div class = "tab trains" data-type = "trains" data-title = "Train, Express"></div> <div class = "tab identity" data-type = "identity" data-title = "Identity"></div> <div class = "tab tech" data-type = "tech" data-title = "Tech"></div> <div class = "tab buildings" data-type = "buildings" data-title = "Buildings"></div> <div class = "tab flags" data-type = "flags" data-title = "Flags"></div> <div class = "tab japanese" data-type = "flags" data-title = "Japanese"></div> </div> <div id = "emoji-results"></div> <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(); document.querySelector("#emoji-output").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. I can't explain this feeling.

// 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, }; let selected = all[type]; document.querySelectorAll(".emoji").forEach(emoji => { emoji.style.display = "none"; }); selected.map(item => { document.getElementById("emoji_id_" + item).style.display = "inline-block" } ); }

But how does the 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")))); }