DEV Community

Cover image for Emoji UI
Alexandr
Alexandr

Posted on

Emoji UI

Emojis are a very interesting subject. It was invented in Japan approximately one human life ago, but nowadays, it's everywhere around the world. And with AI tools using emojis by default, reality became even more saturated with them.

Emojis back then:

Emoji present on the Sharp PI-4000 (1994)

Emoji present on the Sharp PI-4000 (1994)

Emojis now:

Number of the emojis by year

Number of the emojis by year

And one of the most interesting things about emojis is that technically, that's not (just) an image, but a part of the Unicode table, together with other symbols, letters and numbers. Which means, they're (somewhat) omnipresent across devices and platforms.

So, I was thinking, is it possible to create a UI, using only emojis as graphical elements (obviously not counting cards, containers, etc)? Your first response would be - yes, obviously? Why do you even need to write a project for that? But think about it. How close that UI would be to a "serious" one?

Anyway, let's go straight to making it. First of all, I use Svelte simply because I want to see it in action. You're free to use any other framework/library (I wrote the first version of the project in React, but it was obvious overkill) or not use any and enjoy the pure vanilla JS. I've talked with Gemini a bit, and it's supposed to pair emoji UI with glassmorphism, which is...questionable, but we can live with that.
Here is my prompt for GitHub CoPilot to create the first iteration of the project:

Create a Svelte (Vite) app scaffold. UI: Glassmorphism (blur, semi-transparent) using only Unicode/emojis for icons (no SVGs/images). CSS: Pure CSS with a manual Atomic utility system (spacing, flex). Features: Dark mode toggle (Svelte store + prefers-color-scheme). Structure: Top navbar with Emoji tabs to switch views (Buttons, Radios, Inputs) using Svelte transitions. Components: GlassCard.svelte, ThemeToggle.svelte. Use CSS variables for all values.

Results are immediately "playable", but not really impressive. Let's fix that.

Icons

Well, it's the most obvious thing we start with, treating emojis as icons.

I've created the EmojiIcon component:

<script>
  export let icon;
  export let label = ""; 
  export let deco = false;
  export let title = "";

  let className = "";
  export { className as class };
</script>

<span 
  {title}
  role={deco ? null : "img"} 
  aria-label={deco ? null : label} 
  aria-hidden={deco ? "true" : "false"}
  class="sym {className}"
  {...$$restProps}
>
  {icon}
</span>
Enter fullscreen mode Exit fullscreen mode

And here is the result: some emojis, "natural", and slightly changed, using the basic filters.

Emojis, natural and slightly changed

Emojis, natural and slightly changed

Buttons

It's the second simplest component, right after EmojiIcon:

<script>
    import EmojiIcon from "./EmojiIcon.svelte";

    export let icon = "πŸ†—";
    export let text = "";
    export let iconBtn = false; 
    export let reverse = false;

    export let title = "";
    export let ariaLabel = text || title || (iconBtn ? "icon button" : "button");

    let className = "";
    export { className as class };
  </script>

  <button 
    class="{className}" 
    class:btn={!iconBtn} 
    class:btn-icon={iconBtn}
    class:flex-reverse={reverse}
    aria-label={ariaLabel} 
    {title}
    {...$$restProps}
  >
    {#if text && !iconBtn}
      <span class="btn-text">{text}</span>
    {/if}

    <EmojiIcon {icon} deco={true} />
  </button>

  <style>
    button {
      transition: all var(--transition-fast, 150ms);
    }

    .flex-reverse {
      flex-direction: row-reverse;
    }

    .btn-text {
      line-height: 1;
    }
  </style>
Enter fullscreen mode Exit fullscreen mode

And here is the result: buttons and icon buttons, rounded and different in size:

Buttons in different styles

Buttons in different styles

Radiobuttons and checkboxes

If you think about it, radio buttons and checkboxes are extremely similar. The only difference is that radio buttons are pack animals, while checkboxes can coexist in groups but prefer solitary placement. So here is the unified element:

<script>
  export let type = "radio";
  export let group;
  export let value;
  export let label = "";
  export let disabled = false;
  export let theme = "radiobutton";
  export let invert = false; 

  export let off = null;
  export let on = null;

  // default themes
  const themes = {
    radiobutton: { off: "πŸ”˜", on: "⚫" },
    circle: { off: "πŸ”΄", on: "β­•" },
    green_square: { off: "🟩", on: "βœ…" },
    blue_square: { off: "🟦", on: "β˜‘οΈ" },
  };

  $: activeOff = off ?? themes[theme]?.off ?? "πŸ”˜";
  $: activeOn = on ?? themes[theme]?.on ?? "⚫";

  // Manual Checkbox Logic
  function handleCheckbox(e) {
    if (e.target.checked) {
      group = [...group, value];
    } else {
      group = group.filter((v) => v !== value);
    }
  }
</script>

<label
  class="emoji-input"
  class:disabled
  class:is-inverted={invert}
  style="--emoji-off: '{activeOff}'; --emoji-on: '{activeOn}';"
>
  {#if type === "checkbox"}
    <input
      type="checkbox"
      checked={group.includes(value)}
      on:change={handleCheckbox}
      {disabled}
      {...$$restProps}
    />
  {:else}
    <input type="radio" bind:group {value} {disabled} {...$$restProps} />
  {/if}

  <span class="emoji-text">
    <slot>{label}</slot>
  </span>
</label>

<style>
  .emoji-input {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    padding: 0.4rem 0.6rem;
    border-radius: 8px;
    transition: all 0.2s ease;
    width: fit-content;
    user-select: none;
    gap: 0.5rem;
  }

  .emoji-input:not(.disabled):hover {
    background: var(--glass-bg-hover, rgba(255, 255, 255, 0.1));
  }
  .emoji-input:not(.disabled):active {
    transform: scale(0.96);
  }

  input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
  }

  .emoji-text::before {
    display: inline-block;
    width: 1.5em;
    text-align: center;
    font-size: 1.3rem;
    content: var(--emoji-off);
    transition:
      filter 0.2s ease,
      transform 0.2s ease;
  }

  input:checked + .emoji-text::before {
    content: var(--emoji-on);
  }

  .is-inverted input:checked + .emoji-text::before {
    content: var(--emoji-off) !important;
    filter: invert(1);
    transform: scale(1.1);
  }

  .disabled {
    cursor: not-allowed;
    opacity: 0.5;
    filter: grayscale(1);
  }

  [disabled] {
    cursor: not-allowed;
    opacity: 0.5;
    filter: grayscale(1);
    pointer-events: none; /* Stops clicks entirely */
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Techically, you can use whatever you want for your checkboxes and radios, but for the sake of consistency, I've chosen "squares" for checkboxes (🟩 and βœ…, 🟦 and β˜‘οΈ, βœ”οΈ and ❌) and "circles" for radios (πŸ”˜ and inverted πŸ”˜, βšͺ and ⚫, πŸ”΄ and β­• (まる maru, japanese equivalent of a checkmark)). There are more possible pairs, and you're free to experiment, obviously.

Radiobuttons:

Radiobuttons

Radiobuttons

Checkboxes:

Checkboxes

Checkboxes

If you're really invested, you can even make a tri-state checkbox by using βœ…, ❎, and 🟩.

The main element

And here is the main star of the whole project, a mockup of an emoji-driven dashboard, the Emoji Analytics Dashboard (quite a long image, better open it in new tab):

Emoji Analytics Dashboard

Emoji Analytics Dashboard

A few πŸ‘Ž

Unfortunately, the deeper you go, the more and more hidden dangers you'll encounter. One of the most obvious things is that not all emojis are widely available across platforms, systems, and browsers. For example, Chrome on Windows doesn't show country flag emojis because Windows' default font, Segoe UI Emoji, deliberately lacks flag support to avoid political complexities. Instead, Chrome displays two-letter country codes (e.g., "JP" instead of a flag of Japan). That's why there is a thing called RGI, or Recommended for General Interchange, which is "A subset of emojis which is likely to be widely supported across multiple platforms". The keyword is "likely", not guaranteed. Because surprise-surprise, emoji flags are RGI, despite Windows & Chrome combination not representing them properly.
The second thing is that emojis are slightly different platform- and device-wise. It's not that much of a problem, just a mere nuance, but you should keep it in mind.

Crocodile emoji across platforms
🐊 across platforms

Sometimes, even the different versions of the same OS can return different results:

Crocodile emoji Windows versions
🐊 across Windows versions

But there are ways to go around. First of all, you can always check how the emoji would look on different platforms, using an emoji encyclopedia, like emojiterra. And the second is to return the most fitting emoji for the platform, using the userAgent property. Like, here:


const userAgent = window.navigator.userAgent

// Mac
if (userAgent.includes("Macintosh")) {
   // insert Apple-compatible emoji
} 
else if (userAgent.includes("Windows")) {
   // insert Windows-compatible emoji
} 
// etc
Enter fullscreen mode Exit fullscreen mode

Also, there is one more thing, is not that much of a flaw per se, but rather mildly infuriating. Technically, you can generate emojis randomly, no need to store an array of predefined ones. But that would mean a breach of WAI-ARIA accessibility principles, since the Unicode table doesn't (always) store description.

  function getRandomSafeEmoji() {
    const emojiRanges = [
      [0x1f600, 0x1f64f], // Emoticons (Faces, etc.)
      [0x1f300, 0x1f320], // Misc Symbols & Pictographs (Nature, Food)
      [0x1f400, 0x1f4d0], // Animals & Objects
      [0x1f680, 0x1f6c0], // Transport & Map Symbols
    ];

    // Pick a random range from our safe list
    const range = emojiRanges[Math.floor(Math.random() * emojiRanges.length)];

    // Generate a random code point within that range
    const codePoint =
      Math.floor(Math.random() * (range[1] - range[0] + 1)) + range[0];

    return String.fromCodePoint(codePoint);
  }
Enter fullscreen mode Exit fullscreen mode

A way more πŸ‘

Despite all it's flaws, emoji stays a very powerful instrument. You can do a LOT. Like, using emoji as cursor? Not a problem.

  cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='48' viewport='0 0 100 100' style='fill:black;font-size:24px;'><text y='50%'>βœ’οΈ</text></svg>") 16 0, auto;
Enter fullscreen mode Exit fullscreen mode

Using emoji as favicon? Easy.

    <link
      rel="icon"
      type="image/svg+xml"
      href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎨</text></svg>"
    />
Enter fullscreen mode Exit fullscreen mode

Using emojis as the only graphical elements? Yes, definitely. Even that very simple project can show, that you don't need to spend time on choosing the proper icons and stuff. Use emojis to speed up development of the MVP and add all the bells and whistles later.

Don't Be Afraid To Experiment!

You can try live at https://al3xsus.github.io/emoji-ui/
You can see code at https://github.com/al3xsus/emoji-ui

Thanks for your attention. Feel free to share your opinion in comments.

Top comments (0)