@ferrucc-io/emoji-picker vs Frimousse: choosing a React emoji picker
A practical, side-by-side comparison of two modern, composable React emoji pickers.
Disclosure: this comparison is written by Ferruccio Balestreri, the maintainer of @ferrucc-io/emoji-picker. It's an honest look at the trade-offs, including where Frimousse fits, but I'll be upfront that, for most apps, I think shipping the emoji data beats depending on a CDN at runtime.
Both @ferrucc-io/emoji-picker and Frimousse (by Liveblocks) are lightweight, composable, accessible emoji pickers for React. They both render a virtualized list, support keyboard navigation and screen readers, and automatically hide emojis a user's device can't display. If you've outgrown a heavier, all-in-one picker and want something you can fully restyle, either is a strong choice. The differences are in philosophy: how they're styled, where their emoji data comes from, and how much they include out of the box.
TL;DR
| @ferrucc-io/emoji-picker | Frimousse | |
|---|---|---|
| Styling | Tailwind CSS, minimally styled | Headless / unstyled, any CSS |
| Emoji data | Bundled, instant & deterministic | Fetched from a third-party CDN at runtime |
| Works offline | Yes, always | No, needs a network request first |
| Loading state | None; emojis render immediately | Loading / Empty states while it fetches |
| Runtime trust surface | Self-contained, no external origin | Trusts a third-party CDN at runtime |
| Dependencies | Tailwind CSS as a peer | Zero dependencies |
| Batteries | Preview, skin tones, color hover | Minimal primitives, you compose |
| Initial bundle | Larger (includes emoji data) | Smaller, tree-shakable |
| Emoji freshness | Pinned to the version you ship | Always current via CDN |
| React support | React ≥0.14 | React 18 & 19 |
| License | MIT | MIT |
Styling: Tailwind-first vs fully headless
@ferrucc-io/emoji-picker is built around Tailwind CSS. Every part ships minimally styled, and you customize it by passing Tailwind utility classes to the compound components. The README shows Linear- and Slack-style pickers built purely by changing classes. The trade-off is that you add the package to your Tailwind content sources, so Tailwind is effectively a requirement.
Frimousse goes a step further and ships completely unstyled. Each part exposes [frimousse-*] data attributes, and you bring whatever styling layer you like: Tailwind, CSS-in-JS, or plain CSS. This makes it framework-agnostic on the styling side and a natural fit if you're not on Tailwind, or if you want to drop it into a design system like shadcn/ui.
In short: if Tailwind is already your styling language, emoji-picker gives you a head start with sensible defaults; if you want zero styling assumptions, Frimousse is the more neutral primitive.
Emoji data: bundled vs fetched from a CDN
This is the most consequential difference between the two.
@ferrucc-io/emoji-picker bundles its emoji dataset with the component. There is no network request and no loading state. The picker works fully offline and renders instantly, every time, for every user. Because the data is local, nothing leaves your app: there's no third-party request for a corporate firewall, a strict CSP, or a privacy and ad blocker to break, and nothing that can slow down or fail when a user first opens the picker. Its maxUnicodeVersion prop (Unicode 16.0 by default) lets you cap which emoji are shown so nothing renders as a “tofu” box on older systems.
Frimousse takes the opposite approach: the emoji data isn't in the package at all. The first time a user opens the picker, Frimousse has to fetch the dataset (based on Emojibase) from a third-party CDN before it can show anything. That keeps the initial JavaScript bundle small and the emoji set current, but it turns opening an emoji picker into a network operation. Until that request resolves, the user stares at the Loading or Empty state you're required to render. And that request can fail or stall outright: on a flaky connection, fully offline, behind a locked-down corporate network or strict CSP, or with privacy and ad-blocking extensions, the fetch may never complete and the picker can come up empty. You're also betting on an external service's uptime and reachability, and you don't control when the underlying data changes beneath you.
There's a security dimension to this, too. Loading data from a third-party CDN at runtime adds an external entry point to your application's attack surface: every user session reaches out to an origin you don't own or control. If that CDN is ever compromised, hijacked, or has its content tampered with, it can serve whatever it wants back to your users, on your domain, under your app's name, and you are trusting it not to. This is not hypothetical. Supply-chain attacks on previously-trusted CDNs are a recurring, real-world problem; the Polyfill.io incident saw a widely-used CDN start pushing malicious payloads to hundreds of thousands of sites overnight. You can defend against it with Subresource Integrity, a strict CSP, or by self-hosting the data, but that is extra security work you take on and have to keep maintaining. Bundling the emoji data removes the third party from the equation entirely: there is no external origin to trust, no extra endpoint for an attacker to target, and the data you audit at install time is exactly the data your users receive.
For something as small and ubiquitous as an emoji picker, paying a network round-trip, inheriting an external point of failure, and opening a new attack surface whenever the cache isn't warm is a real, recurring cost. @ferrucc-io/emoji-picker sidesteps all of it: the emojis are already there, so the picker is instant, works offline, and behaves identically for every user regardless of network conditions, CSP or privacy tooling. The trade-off is a larger package and bumping a dependency to refresh emoji, a predictable, one-time cost most apps will happily take over a permanent runtime dependency on a CDN. Unless a few kilobytes of bundle are genuinely your tightest constraint, shipping the data is the safer default.
API & composition
Both use a compound-component pattern. emoji-picker leans batteries-included with parts like Header, Preview, SkinTone and Content:
import { EmojiPicker } from "@ferrucc-io/emoji-picker";
<EmojiPicker onEmojiSelect={handleEmojiSelect}>
<EmojiPicker.Header>
<EmojiPicker.Input placeholder="Search emoji" />
</EmojiPicker.Header>
<EmojiPicker.Group>
<EmojiPicker.List />
</EmojiPicker.Group>
</EmojiPicker>Frimousse exposes a leaner set of primitives (Root, Search, Viewport, List) plus the async Loading and Empty slots:
import { EmojiPicker } from "frimousse";
<EmojiPicker.Root>
<EmojiPicker.Search />
<EmojiPicker.Viewport>
<EmojiPicker.Loading>Loading…</EmojiPicker.Loading>
<EmojiPicker.Empty>No emoji found.</EmojiPicker.Empty>
<EmojiPicker.List />
</EmojiPicker.Viewport>
</EmojiPicker.Root>The practical takeaway: emoji-picker gives you more finished pieces (a built-in preview row, skin-tone selector, and dominant-color hover feedback) so you assemble less; Frimousse gives you fewer, smaller primitives so you compose more, with nothing to override.
Dependencies, bundle size & performance
Frimousse markets itself as dependency-free, tree-shakable, and virtualized with minimal re-renders, and because it loads emoji data lazily its initial footprint is small. emoji-picker is also virtualized and self-contained at runtime (no CDN), but it ships the emoji dataset, so its footprint is larger by design and it lists Tailwind as a peer.
Both render only the visible rows of emoji, so scrolling performance is comparable. The bundle-size question really comes back to the data decision above: you either pay for the dataset once in your bundle (emoji-picker) or pay for it over and over as a runtime network fetch plus an external point of failure (Frimousse). A smaller bundle that can't render until a CDN responds isn't free; it just moves the cost somewhere the user feels it.
Ecosystem & framework support
Frimousse targets React 18 and 19 and documents integrations such as a shadcn/ui registry component and pairing with popover libraries. It's maintained by Liveblocks. @ferrucc-io/emoji-picker supports a very wide React peer range, is used in production by teams like June, Langfuse and Typefully, and is maintained by Ferruccio Balestreri. Neither ships a popover, so you pair either with Radix UI, Base UI, Headless UI or React Aria.
When to choose which
Choose @ferrucc-io/emoji-picker if…
- You already use Tailwind CSS.
- You want offline support and zero network requests.
- You want instant render with no loading flash.
- You want built-in previews, skin-tone selection and color hover.
- You want to pin the exact emoji set you ship.
- You'd rather not add a third-party CDN to your security attack surface.
Choose Frimousse if…
- You want zero dependencies and no Tailwind requirement.
- You want the smallest possible initial bundle.
- You want always-current emoji without shipping updates.
- You style with non-Tailwind CSS or a design system.
- You use shadcn/ui and want a registry component.
Installation
# Tailwind-styled, bundled emoji data
npm i @ferrucc-io/emoji-picker
# Headless, CDN-loaded emoji data
npm i frimousseFrequently asked questions
Is @ferrucc-io/emoji-picker or Frimousse better for a React app?
For most apps, @ferrucc-io/emoji-picker is the safer default. It is a batteries-included, Tailwind-styled picker that bundles its emoji data, so the picker is instant, works offline, and never depends on a CDN being reachable. Frimousse is a zero-dependency headless picker, but it fetches its emoji data from a third-party CDN at runtime, which adds a loading state and a network dependency that can fail offline, behind a strict Content-Security-Policy, or with privacy blockers. Choose @ferrucc-io/emoji-picker if you want instant, reliable, offline-capable emoji with previews and skin-tone selection out of the box; choose Frimousse mainly if shaving a few kilobytes off the initial bundle outweighs that runtime dependency.
What is the main difference between the two emoji pickers?
The biggest practical difference is where the emoji data comes from. @ferrucc-io/emoji-picker bundles the emoji dataset with the component, so the picker renders instantly with no network request and no loading state, and keeps working offline. Frimousse instead fetches its emoji data from a third-party CDN (based on Emojibase) the first time the picker opens, so opening the picker becomes a network operation: until the request resolves the user sees a loading or empty state, and if the fetch fails (offline, behind a corporate firewall or strict CSP, or with privacy blockers) the picker can come up empty. That runtime CDN call is also an external dependency on your attack surface. Frimousse trades a smaller initial bundle and always-current emoji for that runtime dependency.
Is loading emoji data from a CDN a security concern?
It is a trade-off worth understanding. Frimousse fetches its emoji data from a third-party CDN at runtime, which adds an external entry point to your application's attack surface: every user session reaches out to an origin you don't own or control. If that CDN is compromised or hijacked it can serve altered content back to your users, and supply-chain incidents like the Polyfill.io attack show this is a real risk with third-party CDNs. You can mitigate it with Subresource Integrity, a strict Content-Security-Policy, or by self-hosting the data, but that is extra work to set up and maintain. @ferrucc-io/emoji-picker avoids the issue by bundling the emoji data in the package, so there is no third-party origin to trust at runtime and the data you audit at install time is exactly what your users receive.
Do both emoji pickers require Tailwind CSS?
No. @ferrucc-io/emoji-picker is designed around Tailwind CSS and expects you to add the package to your Tailwind content sources. Frimousse is completely unstyled and framework-agnostic, so you can style it with Tailwind, CSS-in-JS, or plain CSS targeting its [frimousse-*] data attributes.
Are @ferrucc-io/emoji-picker and Frimousse accessible?
Yes. Both libraries are keyboard navigable and screen-reader friendly, render a virtualized list so only visible emojis are mounted, and automatically hide emojis that the user's system cannot render.
Can I use these emoji pickers as a popover?
Both libraries provide only the picker itself, not a popover or trigger. You pair either one with a popover primitive such as Radix UI, Base UI, Headless UI or React Aria. Frimousse also documents a shadcn/ui registry component if you use that ecosystem.
Which emoji picker has a smaller bundle size?
Frimousse generally has a smaller initial JavaScript footprint because it is dependency-free, tree-shakable, and loads emoji data lazily from a CDN rather than bundling it. But that smaller bundle isn't free: the picker can't render its emoji until a third-party CDN responds, so the cost moves from your bundle to a runtime network request the user waits on (and that can fail). @ferrucc-io/emoji-picker includes its emoji dataset in the package, a larger one-time bundle cost in exchange for instant, offline, dependency-free rendering at runtime.