Emoji Picker — Free HTML CSS JS Snippet

Emoji Picker · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Insert at cursor: selectionStart read + string slice insert + cursor restore
Recent tab: unshift to recent array, deduplicate, cap at 12, re-render on select
6 category tabs: Recent/Smileys/People/Nature/Food/Symbols with CSS .active state
Scale+opacity popup animation: 0.96 scale → 1 on .open class
Hover scale on emoji buttons: transform:scale(1.2) on :hover
Click-outside close: document click + e.target.closest("#emoji-wrap") check
Search input: triggers filterEmoji() on every keystroke
Chat demo: sendMsg() appends outgoing messages, clears input, closes picker

About this UI Snippet

Emoji Picker — Category Tabs, Recent Emojis, Insert at Cursor & Chat Demo

Screenshot of the Emoji Picker snippet rendered live

Type "I'm so happy" into any chat box and it reads flat — type "I'm so happy 🎉" and it suddenly has a tone. That gap is why every messaging product, from the Chat Bubble UI you're replying in to the comment box under a blog post, ships its own emoji picker rather than relying on the operating system's. This snippet builds one from scratch: six categorised tabs (Recent, Smileys, People, Nature, Food, Symbols), a search field, a hover-scale emoji grid, a popover that inserts *exactly where the cursor is* rather than at the end of the line, a self-maintaining "recently used" tab, click-outside dismissal, and a working chat demo so you can see it inserting into a real conversation rather than an empty textarea.

Inserting at the cursor, not the end

The naive approach — input.value += emoji — always appends to the end, which breaks the moment someone clicks back into the middle of a sentence to fix a typo and then wants to drop in an emoji there. insertEmoji(e) instead reads inp.selectionStart, the cursor's character offset, and rebuilds the string around it: inp.value.slice(0, pos) + e + inp.value.slice(pos). The subtle part is what happens *after*: setting .value programmatically silently resets the cursor to position zero, so the very next line restores it with inp.selectionStart = inp.selectionEnd = pos + e.length — placing the caret immediately after the freshly inserted emoji, exactly where a native keyboard insertion would leave it. Skip that one line and every emoji after the first lands in the wrong place.

A "recently used" tab that's really a tiny LRU cache

The Recent tab isn't hand-curated — it's a 12-item array that rebuilds itself on every selection: recent = [e, ...recent.filter(x => x !== e)].slice(0, 12). Unshifting the new emoji to the front, filtering out any earlier occurrence of the same one, and slicing to 12 is the textbook shape of a least-recently-used cache, just applied to emoji instead of memory pages. It's the same "move to front, evict the tail" idea that powers browser history and tab-switcher ordering — useful to recognise because it shows up anywhere a UI claims to remember "recent" anything.

One render function, six categories

Rather than writing six near-identical grid-building blocks, renderGrid(emojis) takes any flat array of emoji strings, clears #emoji-grid, and stamps out a button per emoji with its click handler wired to insertEmoji. showCat(btn, cat) just decides *which* array to hand it — recent for the clock tab, EMOJI_DATA[cat] for everything else — toggles the .active class, and clears any in-progress search. Adding a seventh category is a one-line addition to EMOJI_DATA plus a matching .cat-tab button; the rendering plumbing needs no changes at all.

Closing on an outside click

A single listener on document checks every click against e.target.closest('#emoji-wrap'). If the click landed anywhere outside the trigger-and-popover wrapper, togglePicker() fires and the panel closes — the same delegation pattern used by the Command Palette and Context Menu snippets to dismiss themselves without attaching a listener to every possible "outside" element. The popover itself animates with opacity, pointer-events, and a scale(0.96) → scale(1) transform — properties the compositor can animate without ever triggering layout.

Where the search currently falls short

The filterEmoji(q) stub is honest about its limits: without a names database, "smile" can't be matched to 😀, so it currently just shows a broad slice of every category on any non-empty query. The FAQ below shows how a small emoji-datasource-style package turns this into real name-based search — the kind that lets someone type "fire" and land on 🔥 instead of scrolling six tabs.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of guessing why emoji sometimes land in the wrong spot, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why insertEmoji() must explicitly restore inp.selectionStart and inp.selectionEnd after writing to inp.value, and how the recent array's unshift-filter-slice pattern in insertEmoji implements a tiny least-recently-used cache. The same assistant can help you optimize it, for instance pointing out that filterEmoji() currently ignores the query entirely and just shows a slice of every emoji, and discussing what a real name-based search would need. It's also useful for extending the picker: ask it to wire filterEmoji to an actual emoji-name dataset for real search, persist the recent list to localStorage across sessions, or add keyboard arrow-key navigation through the grid. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an emoji picker popover attached to a chat text input, in plain HTML, CSS, and JavaScript with no library.

Requirements:
- A trigger button that toggles a popover panel containing a search input, a row of category tab buttons (including a "Recent" tab), and a scrollable grid of emoji buttons, animated open and closed with an opacity and scale transition rather than a hard show/hide.
- Clicking any emoji in the grid must insert it into a separate target text input at the exact current cursor position (read via the input's selectionStart), not appended to the end of the input's value, by slicing the existing value into a before-cursor and after-cursor part and rebuilding the string around the inserted emoji.
- After programmatically setting the input's value, explicitly restore the cursor position to immediately after the newly inserted emoji, since setting an input's value property resets the browser's cursor to the start by default.
- Maintain a "recent" list capped at a fixed size (e.g. 12) that updates every time an emoji is inserted: the newly used emoji moves to the front, any earlier occurrence of that same emoji already in the list is removed first so it doesn't appear twice, and the list is trimmed back down to the cap length.
- Build one single grid-rendering function that accepts any flat array of emoji characters and is reused for every category tab and for the recent tab, so switching categories is only a matter of choosing which array to pass in, not duplicating rendering logic per category.
- Close the popover automatically when a click occurs anywhere outside the trigger-and-panel wrapper, using a single document-level click listener with a closest() containment check rather than per-element blur handlers.

Step by step

How to Use

  1. 1
    Click the 😊 button to open the emoji pickerThe picker slides open with a scale animation. Browse categories with the tab buttons. Click any emoji to insert it at the cursor position in the text input.
  2. 2
    Click inside the text input first to set the cursor positionClick in the input where you want the emoji inserted, then open the picker and select an emoji. It inserts exactly at the cursor, not appended to the end.
  3. 3
    Type in the search bar to filter emojisThe search input filters the emoji display. Currently shows a broad set on any search — connect to an emoji names database (emoji-data npm package) for name-based search.
  4. 4
    Check the Recent tab to see your emoji historyThe Recent tab shows the 12 most recently used emojis, updated in real time as you select emojis from other categories. Click Send to send a chat message and test the full flow.
  5. 5
    Attach to any textarea or input in your projectChange document.getElementById("chat-input") to your own input element. The insertEmoji() function works with any text input or textarea by reading selectionStart.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useRef for the input, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

Chat app and messaging interface emoji insertion
The primary use case — a chat input with an emoji picker button. The cursor-position insertion works correctly even when the user has typed partway through a message and wants to insert an emoji in the middle.
Comment and reply system reaction picker
Social platforms and blog comment systems use emoji pickers for reactions and in-text emoji. The recent tab shows the user's favourite emojis for quick repeat use.
Social media post composer emoji support
Content creation tools need emoji pickers for post composers. The picker integrates with any textarea. Add a character counter alongside it to show the user how many characters their post uses including emojis.
Form field emoji support for display names and bios
Profile forms, username fields, and bio textareas often allow emojis. The picker provides a keyboard-free way to select emojis for users on desktop who cannot easily access the system emoji picker.
Study cursor-position text insertion technique
The insertEmoji() function demonstrates the correct pattern for inserting text at cursor position in any input: read selectionStart, slice and reassemble the string, restore cursor position. This pattern applies to any text insertion feature.
No-code builder and drag-drop editor emoji support
Visual page builders and no-code tools let users add emoji to text blocks. An embedded picker provides a better UX than asking users to use their system keyboard shortcut (Win+. or Cmd+Ctrl+Space) which many users do not know.

Got questions?

Frequently Asked Questions

inp.selectionStart returns the current cursor position in the input field (an integer offset from the start). The insertion: inp.value = inp.value.slice(0, selectionStart) + emoji + inp.value.slice(selectionStart). After setting value, cursor is restored: inp.selectionStart = inp.selectionEnd = selectionStart + emoji.length. Without this, setting value programmatically resets the cursor to the end.

Import an emoji metadata package: import emojiData from "emoji-datasource". Each entry has char (the emoji) and name fields. In filterEmoji(q): const matches = emojiData.filter(e => e.name.toLowerCase().includes(q.toLowerCase())).map(e => e.char).slice(0,42). This gives real name-based search ("smiling face", "thumbs up") instead of the current category-level filtering.

Save to localStorage in insertEmoji(): localStorage.setItem("recent_emoji", JSON.stringify(recent)). On page load, restore: const saved = localStorage.getItem("recent_emoji"); if (saved) { try { recent = JSON.parse(saved); } catch {} }. This persists the user's recently used emojis so they appear in the Recent tab on the next visit.

Click "JSX" to download. Use useState for isOpen, curCat, and recent. Use useRef for the input element. In insertEmoji: const pos = inputRef.current.selectionStart; then use a React-controlled input value state with setInputValue(prev => prev.slice(0,pos) + e + prev.slice(pos)). For cursor restore after state update, use useEffect with a ref to the desired cursor position and set selectionStart/End after the render.