I am on a Slack instance with too many emoji. Sometimes I want to quickly find something, sometimes I want to add one to another Slack instance. In all cases, the Slack interface is kind of shit at it. Search is only intermittently fuzzy. Having things local is just nicer if you know what you are doing. It is also simply always a good idea to make backups. As far as I can tell, Slack does not offer an easy way to back up all the emoji (at least as a regular user). Nothing we cannot get around though. I assume here that you can get to https://YOURSLACKINSTANCE.slack.com/customize/emoji. If you cannot, then I cannot help you.

The following JavaScript script needs to be dumped in your developer console. It parses all the emoji it currently spots in the page and adds a listener for any changes to the list of emoji. Whenever a change happens, it reparses things. Is it inefficient? Hell yeah. Do I care for something I am only going to use once? Hell no.

Note: At the time of writing this was for some reason not working in Firefox. It did work in Chrome.

Note: I am assuming you have some knowledge about browsers and browser consoles. Later on I also assume you can handle running a Python script. If you cannot, this is not the place to learn it and I am not the one to teach you.

const all_emoji = new Map();

// Collects all emoji currently listed in the DOM, adds them to all_emoji.
function add_all_current_emoji() {
  const rows = document.querySelectorAll("div.p-customize_emoji_list__row");
  for (const row of rows) {
    const image = row.querySelector("img.p-customize_emoji_list__image");
    const trigger = row.querySelector("b.black");
    all_emoji.set(trigger.innerText, image.src);


// Now listen for changes to the emoji list
const emoji_list = document.querySelector(
const config = { childList: true };
const callback = (mutation_list, observer) => {
  for (const mutation of mutation_list) {
    if (mutation.type === "childList") {
      // Ideally you would only look at added nodes, but I will be lazy (and
      // inefficient) and just reparse the entire list currently in the DOM.
      // mutation.addedNodes is NodeList of added nodes.
      console.log(`Current emoji map size: ${all_emoji.size}`);

const observer = new MutationObserver(callback);
observer.observe(emoji_list, config);

Now the most annoying part, you have to start scrolling through the emoji list. If you only have a few 100, this will not take long. If you have 8023 as I have on this instance, it will take a bit longer. Every 100 emoji there will be a slight delay as Slack fetches the rest from the network. Just keep going steadily to the end. You probably do not want to scroll past a bunch of them, but I have not tested whether this matters or not. As mentioned: it is a one time thing. The script will be spitting out updates on the number of collected emoji as it runs. If it is not, something could be wrong.

Once you have reached the end, you want to verify that the script mentioned as many emoji as Slack reports at the top of the page.

Next up is grabbing all that data. I opt to throw it into a CSV file. Just run the following JavaScript.

function map_to_csv(m) {
  m.forEach((url, trigger) => {
    s += `\n${trigger},${url}`;
  return s;

This will spit out CSV content on the console. Copy it all (Chrome has a “Copy” button that appears). Paste it into a file and verify the number of lines matches the number of emoji. Save the file as slack-emoji.csv. Edit: Actually, this will give you a line too many. The first line will be empty. Just delete that one.

Now to actually fetch all the images locally, I whipped together the following Python script. It should gracefully handle getting interrupted. It only fetches an image every three seconds to not blast the server (and to not get rate limited or banned by the server). I am placing this script in a folder in which I also have the slack-emoji.csv file.

import csv
import os
import time
import urllib.request

def emoji_to_path(emoji):
    extension = emoji["url"].split(".")[-1]
    urlencoded_name = emoji["url"].split("/")[-2]
    name = emoji["name"]
    name = name[1:-1]
    if name != urlencoded_name:
        return "out/funky/" + urlencoded_name + "." + extension
        return "out/" + name + "." + extension

# Create directories
except FileExistsError:
except FileExistsError:

ctr = 0

with open("slack-emoji.csv", "r") as csvfile:
    reader = csv.DictReader(csvfile, fieldnames=["name", "url"])
    for row in reader:
        ctr += 1

        p = emoji_to_path(row)

        if os.path.exists(p):
                "Emoji #"
                + str(ctr)
                + " "
                + row["name"]
                + " already exists at "
                + p
                + ". Skipping."

        # It says urlretrieve is part of the legacy interface and might become
        # deprecated in the future. This does not seem to be the case yet in
        # Python 3.12, soooo just using that.
        urllib.request.urlretrieve(row["url"], filename=p)

        print("Handled emoji #" + str(ctr) + " " + row["name"])

        # Do not blast Slack servers

Running it will create a folder out and a folder out/funky. out contains all images for which the name in the url matches the trigger name. out/funky contains images for which this is not the case. Those are saved as the url name. I think, I hope, this way there should not be any overlap.

None of the scripts take aliases into account. An alias enables pointing different names to the same image. This situation should lead to the original being present in out and it being present again in out/funky with the name of the original. Using the logic in emoji_to_path should tell you where to look.

Now that you have all of these locally, you can do whatever you want with it. I decided to write this blog post while waiting for my downloads to finish and that process is still ongoing, so I cannot help you with the next step.