Face

Frimousse

v0.4.0

A lightweight, unstyled, and composable emoji picker for React.

🐰 0
🥚 0
npm i frimousse

If you are using shadcn/ui, you can also install it as a pre-built component via the shadcn CLI.

npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker

Learn more in the shadcn/ui section.

Import the EmojiPicker parts and create your own component by composing them.

import { EmojiPicker } from "frimousse";

export function MyEmojiPicker() {
  return (
    <EmojiPicker.Root>
      <EmojiPicker.Search />
      <EmojiPicker.Viewport>
        <EmojiPicker.Loading>Loading…</EmojiPicker.Loading>
        <EmojiPicker.Empty>No emoji found.</EmojiPicker.Empty>
        <EmojiPicker.List />
      </EmojiPicker.Viewport>
    </EmojiPicker.Root>
  );
}

Apart from a few sizing and overflow defaults, the parts don’t have any styles out-of-the-box. Being composable, you can bring your own styles and apply them however you want: Tailwind CSS, CSS-in-JS, vanilla CSS via inline styles, classes, or by targeting the [frimousse-*] attributes present on each part.

"use client";

import { EmojiPicker } from "frimousse";

export function MyEmojiPicker() {
  return (
    <EmojiPicker.Root className="isolate flex h-[368px] w-fit flex-col bg-white dark:bg-neutral-900">
      <EmojiPicker.Search className="z-10 mx-2 mt-2 appearance-none rounded-md bg-neutral-100 px-2.5 py-2 text-sm dark:bg-neutral-800" />
      <EmojiPicker.Viewport className="relative flex-1 outline-hidden">
        <EmojiPicker.Loading className="absolute inset-0 flex items-center justify-center text-neutral-400 text-sm dark:text-neutral-500">
          Loading…
        </EmojiPicker.Loading>
        <EmojiPicker.Empty className="absolute inset-0 flex items-center justify-center text-neutral-400 text-sm dark:text-neutral-500">
          No emoji found.
        </EmojiPicker.Empty>
        <EmojiPicker.List
          className="select-none pb-1.5"
          components={{
            CategoryHeader: ({ category, ...props }) => (
              <div
                className="bg-white px-3 pt-3 pb-1.5 font-medium text-neutral-600 text-xs dark:bg-neutral-900 dark:text-neutral-400"
                {...props}
              >
                {category.label}
              </div>
            ),
            Row: ({ children, ...props }) => (
              <div className="scroll-my-1.5 px-1.5" {...props}>
                {children}
              </div>
            ),
            Emoji: ({ emoji, ...props }) => (
              <button
                className="flex size-8 items-center justify-center rounded-md text-lg data-[active]:bg-neutral-100 dark:data-[active]:bg-neutral-800"
                {...props}
              >
                {emoji.emoji}
              </button>
            ),
          }}
        />
      </EmojiPicker.Viewport>
    </EmojiPicker.Root>
  );
}

You might want to use it in a popover rather than on its own. Frimousse only provides the emoji picker itself so if you don’t have a popover component in your app yet, there are several libraries available: Radix UI, Base UI, Headless UI, and React Aria, to name a few.

If you are using shadcn/ui, you can install a pre-built version of the component which integrates with the existing shadcn/ui variables via the shadcn CLI.

npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker
"use client";

import * as React from "react";

import {
  EmojiPicker,
  EmojiPickerSearch,
  EmojiPickerContent,
} from "@/components/ui/emoji-picker";

export default function Page() {
  return (
    <main className="flex h-full min-h-screen w-full items-center justify-center p-4">
      <EmojiPicker
        className="h-[326px] rounded-lg border shadow-md"
        onEmojiSelect={({ emoji }) => {
          console.log(emoji);
        }}
      >
        <EmojiPickerSearch />
        <EmojiPickerContent />
      </EmojiPicker>
    </main>
  );
}

It can be composed and combined with other shadcn/ui components like Popover.

"use client";

import * as React from "react";

import { Button } from "@/components/ui/button";
import {
  EmojiPicker,
  EmojiPickerSearch,
  EmojiPickerContent,
  EmojiPickerFooter,
} from "@/components/ui/emoji-picker";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

export default function Page() {
  const [isOpen, setIsOpen] = React.useState(false);

  return (
    <main className="flex h-full min-h-screen w-full items-center justify-center p-4">
      <Popover onOpenChange={setIsOpen} open={isOpen}>
        <PopoverTrigger asChild>
          <Button>Open emoji picker</Button>
        </PopoverTrigger>
        <PopoverContent className="w-fit p-0">
          <EmojiPicker
            className="h-[342px]"
            onEmojiSelect={({ emoji }) => {
              setIsOpen(false);
              console.log(emoji);
            }}
          >
            <EmojiPickerSearch />
            <EmojiPickerContent />
            <EmojiPickerFooter />
          </EmojiPicker>
        </PopoverContent>
      </Popover>
    </main>
  );
}

Use the resolveEmojiData prop to load emoji data from your own source or support locales Emojibase doesn’t provide. Return your data for the locales you handle and delegate the rest to defaultEmojiDataResolver.

import { defaultEmojiDataResolver, EmojiPicker } from "frimousse";
import { emojiData } from "./emoji-data";

<EmojiPicker.Root
  locale="ne"
  resolveEmojiData={(locale, options) =>
    locale === "ne"
      ? emojiData
      : defaultEmojiDataResolver(locale, options)
  }
>
  {/* ... */}
</EmojiPicker.Root>

The data should use the EmojiData type. For example, emoji-data.ts could contain:

import type { EmojiData } from "frimousse";

export const emojiData: EmojiData = {
  locale: "ne",
  emojis: [
    {
      emoji: "😀",
      label: "हाँसेको अनुहार",
      tags: ["खुसी"],
      category: 0,
      version: 1,
    },
  ],
  categories: [{ index: 0, label: "अनुहारहरू" }],
  skinTones: {
    light: "हल्का",
    "medium-light": "मध्यम हल्का",
    medium: "मध्यम",
    "medium-dark": "मध्यम गाढा",
    dark: "गाढा",
  },
};

Each emoji’s category matches a category’s index. Category labels and skinTones are localized names shown in the picker. Data must describe standard Unicode emojis rendered as text; image and sprite-based emojis aren’t supported.

Any string can be used as a locale. The default resolver falls back to en when Emojibase doesn’t support it. Frimousse uses custom data as-is; filter unsupported emoji versions and country flags before returning it if needed.

A resolver can return data or a promise. To cache custom data locally, use createEmojiDataCache.

Use useEmojiDetails to load a stored emoji’s localized label and search tags in a React component. It works without mounting a picker and shares its cached data with pickers using the same locale and source.

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useEmojiDetails } from "frimousse";

function EmojiLabel() {
  const emoji = useEmojiDetails("👍🏽", { locale: "en" });

  return <span>{emoji?.label ?? "Unknown emoji"}</span>;
}

<ErrorBoundary fallback={<span>Could not load emoji</span>}>
  <Suspense fallback={<span>Loading…</span>}>
    <EmojiLabel />
  </Suspense>
</ErrorBoundary>

The component calling the hook must be inside a Suspense boundary. Loading errors reach the nearest error boundary; this example uses the react-error-boundary package.

Skin-tone variants return the base emoji’s details. Both and ❤️ match the same emoji too. If no match exists, the hook returns undefined after loading completes.

For a synchronous lookup, use getEmojiDetails. It reads data already loaded by a picker or the hook and returns undefined if that data isn’t loaded yet. It doesn’t load data or update your component when data becomes available.

import { getEmojiDetails } from "frimousse";

const emoji = getEmojiDetails("👍🏽", { locale: "en" });

By default, both APIs include emojis the browser can’t render.

Various styling-related details and examples.

The emoji picker doesn’t require hard-coded dimensions and instead supports dynamically adapting to the contents (e.g. the number of columns, the size of the rows, the padding within the sticky category headers, etc). One aspect to keep in mind is that inner components within EmojiPicker.List should be of the same size (e.g. all rows should be of the same height) to prevent layout shifts.

The --frimousse-viewport-width CSS variable can be used as a max-width to prevent some areas from becoming wider than the automatically sized contents, when showing the hovered emoji’s name below for example.

And although not required, it’s still possible to force the emoji picker and its contents to be of a specific width, to fit the viewport on mobile for example.

Because of its virtualized nature, adding padding to EmojiPicker.List can be tricky. We recommend adding horizontal padding to rows and category headers, and vertical padding on the list itself. Finally, to apply the same vertical padding to the viewport when keyboard navigating (which automatically scrolls to out-of-view rows), you can set the same value as scroll-margin-block on rows.

Some emoji pickers like Slack’s display their emoji buttons with seemingly random background colors when active (either hovered or selected via keyboard navigation). This can be achieved by using :nth-child selectors on rows and emojis to alternate through a list of colors. In the example below, a row’s first emoji has a red background, the second green, the third blue, then red again, and so on. All odd rows follow the same pattern, while even rows offset it by one to avoid every column using the same color, starting with blue instead of red.

<EmojiPickerPrimitive.List
  components={{
    Row: ({ children, ...props }) => (
      <div className="group" {...props}>
        {children}
      </div>
    ),
    Emoji: ({ emoji, ...props }) => {
      return (
        <button
          className="relative flex aspect-square size-8 items-center justify-center overflow-hidden rounded-md text-lg data-[active]:group-even:nth-[3n+1]:bg-blue-100 data-[active]:group-even:nth-[3n+2]:bg-red-100 data-[active]:group-even:nth-[3n+3]:bg-green-100 data-[active]:group-odd:nth-[3n+1]:bg-red-100 data-[active]:group-odd:nth-[3n+2]:bg-green-100 data-[active]:group-odd:nth-[3n+3]:bg-blue-100 dark:data-[active]:group-even:nth-[3n+1]:bg-blue-900 dark:data-[active]:group-even:nth-[3n+2]:bg-red-900 dark:data-[active]:group-even:nth-[3n+3]:bg-green-900 dark:data-[active]:group-odd:nth-[3n+1]:bg-red-900 dark:data-[active]:group-odd:nth-[3n+2]:bg-green-900 dark:data-[active]:group-odd:nth-[3n+3]:bg-blue-900"
          {...props}
        >
          {emoji.emoji}
        </button>
      );
    },
  }}
/>

Some other emoji pickers like Linear’s use the main color from the button’s emoji as background color instead. Extracting colors from emojis isn’t trivial, but a similar visual result can be achieved more easily by duplicating the emoji and scaling it to fill the background, then blurring it. In the example below, the blurred and duplicated emoji is built as a ::before pseudo-element.

<EmojiPickerPrimitive.List
  components={{
    Emoji: ({ emoji, ...props }) => {
      return (
        <button
          className="relative flex aspect-square size-8 items-center justify-center overflow-hidden rounded-md text-lg data-[active]:bg-neutral-100/80 dark:data-[active]:bg-neutral-800/80 before:absolute before:inset-0 before:-z-1 before:hidden before:items-center before:justify-center before:text-[2.5em] before:blur-lg before:saturate-200 before:content-(--emoji) data-[active]:before:flex"
          style={
            {
              "--emoji": `"${emoji.emoji}"`,
            } as CSSProperties
          }
          {...props}
        >
          {emoji.emoji}
        </button>
      );
    },
  }}
/>

All parts, hooks, and helpers, along their usage and options.

Surrounds all the emoji picker parts.

<EmojiPicker.Root onEmojiSelect={({ emoji }) => console.log(emoji)}>
  <EmojiPicker.Search />
  <EmojiPicker.Viewport>
    <EmojiPicker.List />
  </EmojiPicker.Viewport>
</EmojiPicker.Root>

Options affecting the entire emoji picker are available on this component as props.

<EmojiPicker.Root locale="fr" columns={10} skinTone="medium">
  {/* ... */}
</EmojiPicker.Root>

A search input to filter the list of emojis.

<EmojiPicker.Root>
  <EmojiPicker.Search />
  <EmojiPicker.Viewport>
    <EmojiPicker.List />
  </EmojiPicker.Viewport>
</EmojiPicker.Root>

It can be controlled or uncontrolled.

const [search, setSearch] = useState("");

return (
  <EmojiPicker.Root>
    <EmojiPicker.Search
      value={search}
      onChange={(event) => setSearch(event.target.value)}
    />
    {/* ... */}
  </EmojiPicker.Root>
);

The scrolling container of the emoji picker.

<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 list of emojis.

<EmojiPicker.Root>
  <EmojiPicker.Search />
  <EmojiPicker.Viewport>
    <EmojiPicker.List />
  </EmojiPicker.Viewport>
</EmojiPicker.Root>

Inner components within the list can be customized via the components prop.

<EmojiPicker.List
  components={{
    CategoryHeader: ({ category, ...props }) => (
      <div {...props}>{category.label}</div>
    ),
    Emoji: ({ emoji, ...props }) => (
      <button {...props}>
        {emoji.emoji}
      </button>
    ),
    Row: ({ children, ...props }) => <div {...props}>{children}</div>,
  }}
/>

Only renders when the emoji data is loading.

<EmojiPicker.Root>
  <EmojiPicker.Search />
  <EmojiPicker.Viewport>
    <EmojiPicker.Loading>Loading…</EmojiPicker.Loading>
    <EmojiPicker.List />
  </EmojiPicker.Viewport>
</EmojiPicker.Root>

Only renders when no emoji is found for the current search.

<EmojiPicker.Root>
  <EmojiPicker.Search />
  <EmojiPicker.Viewport>
    <EmojiPicker.Empty>No emoji found.</EmojiPicker.Empty>
    <EmojiPicker.List />
  </EmojiPicker.Viewport>
</EmojiPicker.Root>

It can also expose the current search via a render callback to build a more detailed empty state.

<EmojiPicker.Empty>
  {({ search }) => <>No emoji found for "{search}"</>}
</EmojiPicker.Empty>

A button to change the current skin tone by cycling through the available skin tones.

<EmojiPicker.SkinToneSelector />

The emoji used as visual can be customized.

<EmojiPicker.SkinToneSelector emoji="👋" />

If you want to build a custom skin tone selector, you can use the EmojiPicker.SkinTone component or the useSkinTone hook.

Exposes the current skin tone and a function to change it via a render callback.

<EmojiPicker.SkinTone>
  {({ skinTone, setSkinTone }) => (
    <div>
      <span>{skinTone}</span>
      <button onClick={() => setSkinTone("none")}>Reset skin tone</button>
    </div>
  )}
</EmojiPicker.SkinTone>

It can be used to build a custom skin tone selector: pass an emoji you want to use as visual and it will return its skin tone variations.

const [skinTone, setSkinTone, skinToneVariations] = useSkinTone("👋");

// (👋) (👋🏻) (👋🏼) (👋🏽) (👋🏾) (👋🏿)
<EmojiPicker.SkinTone emoji="👋">
  {({ skinTone, setSkinTone, skinToneVariations }) => (
    skinToneVariations.map(({ skinTone, emoji }) => (
      <button key={skinTone} onClick={() => setSkinTone(skinTone)}>
        {emoji}
      </button>
    ))
  )}
</EmojiPicker.SkinTone>

If you prefer to use a hook rather than a component, useSkinTone is also available.

An already-built skin tone selector is also available, EmojiPicker.SkinToneSelector.

Exposes the currently active emoji (either hovered or selected via keyboard navigation) via a render callback.

<EmojiPicker.ActiveEmoji>
  {({ emoji }) => <span>{emoji}</span>}
</EmojiPicker.ActiveEmoji>

It can be used to build a preview area next to the list.

<EmojiPicker.ActiveEmoji>
  {({ emoji }) => (
    <div>
      {emoji ? (
        <span>{emoji.emoji} {emoji.label}</span>
      ) : (
        <span>Select an emoji…</span>
      )}
    </div>
  )}
</EmojiPicker.ActiveEmoji>

If you prefer to use a hook rather than a component, useActiveEmoji is also available.

Returns the current skin tone and a function to change it.

const [skinTone, setSkinTone] = useSkinTone();

It can be used to build a custom skin tone selector: pass an emoji you want to use as visual and it will return its skin tone variations.

const [skinTone, setSkinTone, skinToneVariations] = useSkinTone("👋");

// (👋) (👋🏻) (👋🏼) (👋🏽) (👋🏾) (👋🏿)
skinToneVariations.map(({ skinTone, emoji }) => (
  <button key={skinTone} onClick={() => setSkinTone(skinTone)}>
    {emoji}
  </button>
));

If you prefer to use a component rather than a hook, EmojiPicker.SkinTone is also available.

An already-built skin tone selector is also available, EmojiPicker.SkinToneSelector.

Returns the currently active emoji (either hovered or selected via keyboard navigation).

const activeEmoji = useActiveEmoji();

It can be used to build a preview area next to the list.

const activeEmoji = useActiveEmoji();

<div>
  {activeEmoji ? (
    <span>{activeEmoji.emoji} {activeEmoji.label}</span>
  ) : (
    <span>Select an emoji…</span>
  )}
</div>

If you prefer to use a component rather than a hook, EmojiPicker.ActiveEmoji is also available.

Loads and caches Emojibase data, filtering out emojis the browser can’t render. It is the default resolver for EmojiPicker.Root and can be used for locales a custom resolver doesn’t handle.

import { defaultEmojiDataResolver } from "frimousse";

const data = await defaultEmojiDataResolver("fr", {});

Returns a promise of EmojiData, in the format shown in the custom data example. Loading errors reject the promise.

Creates a cache for storing custom emoji data in localStorage. Read and write entries inside your resolver to reuse data across visits. Use your own cache name to keep it separate from the default resolver’s data.

import {
  createEmojiDataCache,
  defaultEmojiDataResolver,
  type EmojiData,
  type EmojiDataResolver,
} from "frimousse";

const cache = createEmojiDataCache({ name: "my-app/emoji-data" });

export const resolveEmojiData: EmojiDataResolver = async (
  locale,
  options,
) => {
  if (locale !== "ne") {
    return defaultEmojiDataResolver(locale, options);
  }

  const cached = cache.get(locale);

  if (cached) {
    return cached.data;
  }

  const response = await fetch("/emoji-data/ne.json", {
    signal: options.signal,
  });

  if (!response.ok) {
    throw new Error("Could not load emoji data");
  }

  const data: EmojiData = await response.json();
  cache.set(locale, data);
  return data;
};

The JSON file uses the same format as the custom data example. Pass this resolveEmojiData function to EmojiPicker.Root. Entries stay cached until you replace or remove them; the cache doesn’t check for updates automatically.

If browser storage is unavailable, reads return null and writes are ignored.

Synchronously reads an emoji’s localized label and search tags from data already loaded in memory by a picker or useEmojiDetails. It never fetches data, reads browser storage, or calls a resolver.

import { getEmojiDetails } from "frimousse";

const emoji = getEmojiDetails("👍🏽", { locale: "en" });

emoji?.label; // "Thumbs up"
emoji?.emoji; // "👍"

By default, data is shared with the picker and includes emojis the browser can’t render. A custom resolver determines which emojis are available for lookup.

Returns EmojiDetails, or undefined if the dataset hasn’t been loaded or no match exists. Use useEmojiDetails to load missing data and update a React component when it becomes available.

EmojiDetails has the following fields:

Returns an emoji’s localized details, suspending while data loads and throwing loading errors to the nearest error boundary. It works outside EmojiPicker.Root and shares cached data and pending requests with the default picker.

import { useEmojiDetails } from "frimousse";

const emoji = useEmojiDetails("👍🏽", {
  locale: "en",
});

Accepts the same emoji and options as getEmojiDetails. Changing the locale or source loads the corresponding dataset; changing only the emoji reuses the loaded data. Requests continue if a suspended component unmounts or changes source, so other consumers can reuse their results.

A custom resolveEmojiData is called when its dataset isn’t cached. Define the resolver outside the component so its reference survives suspended renders; useCallback inside a component that suspends on its first render isn’t sufficient. Hooks using the same function, locale, emojiVersion, and emojibaseUrl share a request. The resolver receives the locale and those source options, without an AbortSignal.

Custom resolvers control persistent caching. Their completed data is shared with pickers and synchronous lookups using the same function and options. Return a new emojis array when its entries change.

Returns EmojiDetails, with the same fields and skin-tone behavior as getEmojiDetails, or undefined if no match exists in the loaded dataset. While loading, the nearest Suspense boundary displays its fallback. Server rendering also displays that fallback; data loads only on the client.

Loading failures are cached and thrown to the nearest error boundary. Resetting the boundary alone doesn’t retry the request. A successful load of the same source through a picker clears the cached failure; reset the boundary after that load, or reload the page to start fresh.

The name “frimousse” means “little face” in French, and it can also refer to smileys and emoticons.

The emoji picker component was originally created for the Liveblocks Comments default components, within @liveblocks/react-ui.

The emoji data is based on Emojibase.