Face

Frimousse

v0.1.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>
  );
}

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 and hooks, 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.

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.