Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

- Add `resolveEmojiData` prop on `EmojiPicker.Root` to control how emoji data is resolved, with a `(locale, { emojiVersion, emojibaseUrl, signal }) => EmojiData | Promise<EmojiData>` function. It defaults to `defaultEmojiDataResolver`, now exported, which fetches Emojibase data as before—so custom resolvers can handle the locales they know about (including ones not supported by Emojibase) and delegate the rest to it.
- Add `createEmojiDataCache` to persist emoji data in `localStorage` across page loads, the same cache used by `defaultEmojiDataResolver`.
- Fix `sessionStorage` revalidation being skipped for every locale after the first one was fetched.

## [0.3.0] - 2025-07-15

- Add `sticky` prop on `EmojiPicker.Root` to allow disabling sticky category headers, thanks @Earthsplit!
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,64 @@ npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker

It can be composed and combined with other shadcn/ui components like [Popover](https://ui.shadcn.com/docs/components/popover).

### Custom emoji data & locales

Emoji data is resolved by a function, and the `resolveEmojiData` prop on `EmojiPicker.Root` lets you replace it. Its default, `defaultEmojiDataResolver`, fetches [Emojibase](https://emojibase.dev/) data from a CDN (cached in `localStorage`/`sessionStorage`) for the [locales it supports](https://emojibase.dev/docs/datasets/#localization).

If you already have emoji data in memory, or need a locale Emojibase doesn’t provide, return it from your own resolver and delegate everything else to the default one.

```tsx
import { EmojiPicker, defaultEmojiDataResolver, type EmojiData } from "frimousse";

const myEmojiData: Record<string, EmojiData> = {
tr: {
locale: "tr",
emojis: [
/* … */
],
categories: [
/* … */
],
skinTones: {
/* … */
},
},
};

<EmojiPicker.Root
locale={locale}
resolveEmojiData={(locale, options) =>
myEmojiData[locale] ?? defaultEmojiDataResolver(locale, options)
}
/>;
```

A resolver can return data synchronously or asynchronously, and receives the current locale along with `{ emojiVersion, emojibaseUrl, signal }`. Any string is accepted as a `locale`, and it’s only validated by `defaultEmojiDataResolver` (which falls back to `"en"` for locales Emojibase doesn’t support). Data you return yourself is used exactly as provided—no version or country-flag filtering is applied, so pre-filter it if needed.

Resolvers are called once per locale (they’re not re-run when their identity changes), but if resolving is expensive you can cache the result across page loads with `createEmojiDataCache`, the same `localStorage` cache `defaultEmojiDataResolver` uses.

```tsx
import { createEmojiDataCache, type EmojiDataResolver } from "frimousse";

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

const resolveEmojiData: EmojiDataResolver = async (locale, options) => {
const cached = cache.get(locale);

if (cached) {
return cached.data;
}

const data = await fetchMyEmojiData(locale, options);

cache.set(locale, data);

return data;
};
```

The data must describe standard Unicode emoji rendered as text; custom image or sprite-based emoji aren’t supported.

## Documentation

Find the full documentation and examples on [frimousse.liveblocks.io](https://frimousse.liveblocks.io).
Expand Down
15 changes: 1 addition & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@
"turbo": "^2.4.4",
"typescript": "^5.8.2",
"vitest": "^3.0.8",
"vitest-browser-react": "^0.1.1",
"vitest-fetch-mock": "^0.4.5"
"vitest-browser-react": "^0.1.1"
},
"bugs": {
"url": "https://github.com/liveblocks/frimousse/issues"
Expand Down
5 changes: 4 additions & 1 deletion site/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# https://liveblocks.io/dashboard/apikeys
LIVEBLOCKS_SECRET_KEY=
#
# Copy this file to `.env.local`. The placeholder below is enough to run the
# site locally, only the reactions in the header need a real key to connect.
LIVEBLOCKS_SECRET_KEY=sk_dev_placeholder
115 changes: 114 additions & 1 deletion site/src/components/sections/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CircleHelp } from "lucide-react";
import type { ComponentProps } from "react";
import { ColorfulButtonsAlternate } from "@/examples/colorful-buttons/colorful-buttons-alternate";
import { ColorfulButtonsBlur } from "@/examples/colorful-buttons/colorful-buttons-blur";
import { CustomEmojiData } from "@/examples/custom-emoji-data/custom-emoji-data";
import { ShadcnUi } from "@/examples/shadcnui/shadcnui";
import { ShadcnUiPopover } from "@/examples/shadcnui/shadcnui-popover";
import { Usage } from "@/examples/usage/usage";
Expand Down Expand Up @@ -151,6 +152,81 @@ export function Docs({
</p>
<ShadcnUiPopover />

<PermalinkHeading as="h3">
Custom emoji data &amp; locales
</PermalinkHeading>
<p>
Emoji data is resolved by a function, and the{" "}
<a href="#emojipicker.root-props">
<code>resolveEmojiData</code>
</a>{" "}
prop lets you replace it. Its default,{" "}
<code>defaultEmojiDataResolver</code>, fetches{" "}
<a href="https://emojibase.dev/" rel="noreferrer" target="_blank">
Emojibase
</a>{" "}
data from a CDN (cached in <code>localStorage</code> and{" "}
<code>sessionStorage</code>) for the{" "}
<a
href="https://emojibase.dev/docs/datasets/#localization"
rel="noreferrer"
target="_blank"
>
locales it supports
</a>
.
</p>
<p>
If you already have emoji data in memory, or need a locale Emojibase
doesn’t provide, return it from your own resolver and delegate
everything else to the default one.
</p>
<CustomEmojiData />
<p>
A resolver can return data synchronously or asynchronously, and receives
the current locale along with{" "}
<code>
{"{"} emojiVersion, emojibaseUrl, signal {"}"}
</code>
. Any string is accepted as a{" "}
<a href="#emojipicker.root-props">locale</a>, and it’s only validated by{" "}
<code>defaultEmojiDataResolver</code> (which falls back to{" "}
<code>en</code> for locales Emojibase doesn’t support). Data you return
yourself is used exactly as provided, no{" "}
<a href="#emojipicker.root-props">Emoji version</a> or country flag
filtering is applied, so pre-filter it if needed.
</p>
<p>
Resolvers are called once per locale, they aren’t re-run when their
identity changes. If resolving is expensive, the result can be cached
across page loads with <code>createEmojiDataCache</code>, the same{" "}
<code>localStorage</code> cache <code>defaultEmojiDataResolver</code>{" "}
uses.
</p>
<CodeBlock lang="tsx">{`
import { createEmojiDataCache, type EmojiDataResolver } from "frimousse";

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

const resolveEmojiData: EmojiDataResolver = async (locale, options) => {
const cached = cache.get(locale);

if (cached) {
return cached.data;
}

const data = await fetchMyEmojiData(locale, options);

cache.set(locale, data);

return data;
};
`}</CodeBlock>
<p>
The data must describe standard Unicode emojis rendered as text, custom
image or sprite-based emojis aren’t supported.
</p>

<PermalinkHeading as="h2">Styling</PermalinkHeading>
<p>Various styling-related details and examples.</p>

Expand Down Expand Up @@ -295,8 +371,20 @@ export function Docs({
<PropertiesListRow name="onEmojiSelect" type="(emoji: Emoji) => void">
<p>A callback invoked when an emoji is selected.</p>
</PropertiesListRow>
<PropertiesListRow defaultValue={`"en"`} name="locale" type="Locale">
<PropertiesListRow
defaultValue={`"en"`}
name="locale"
type="Locale | (string & {})"
>
<p>The locale of the emoji picker.</p>
<p>
Any string is accepted, locales outside of the built-in{" "}
<code>Locale</code> list can be used with a custom{" "}
<a href="#emojipicker.root-props">
<code>resolveEmojiData</code>
</a>
.
</p>
</PropertiesListRow>
<PropertiesListRow
defaultValue={`"none"`}
Expand Down Expand Up @@ -367,6 +455,31 @@ export function Docs({
locale’s directory needs to be hosted instead of the entire package.
</p>
</PropertiesListRow>
<PropertiesListRow
defaultValue="defaultEmojiDataResolver"
name="resolveEmojiData"
type="EmojiDataResolver"
>
<p>
A function returning the emoji data for the current locale, either
synchronously or asynchronously. It receives the locale along with{" "}
<code>
{"{"} emojiVersion, emojibaseUrl, signal {"}"}
</code>
.
</p>
<p>
By default, <code>defaultEmojiDataResolver</code> fetches{" "}
<a href="https://emojibase.dev/" rel="noreferrer" target="_blank">
Emojibase
</a>{" "}
data from a CDN. Learn more in the{" "}
<a href="#custom-emoji-data-and-locales">
custom emoji data &amp; locales
</a>{" "}
section.
</p>
</PropertiesListRow>
<PropertiesListBasicRow>
<p>
All built-in <code>div</code> props.
Expand Down
105 changes: 105 additions & 0 deletions site/src/examples/custom-emoji-data/custom-emoji-data.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import {
defaultEmojiDataResolver,
type EmojiData,
type EmojiDataResolver,
EmojiPicker as EmojiPickerPrimitive,
type EmojiPickerRootProps,
} from "frimousse";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ExamplePreview } from "@/examples/example-preview";
import { toast } from "@/lib/toast";
import { cn } from "@/lib/utils";
import { turkishEmojiData } from "./turkish-emoji-data";

const LOCALES = [
{ locale: "en", label: "English" },
{ locale: "fr", label: "Français" },
{ locale: "tr", label: "Türkçe" },
];

const myEmojiData: Record<string, EmojiData> = {
tr: turkishEmojiData,
};

const resolveEmojiData: EmojiDataResolver = (locale, options) =>
myEmojiData[locale] ?? defaultEmojiDataResolver(locale, options);

function EmojiPicker({ className, columns, ...props }: EmojiPickerRootProps) {
return (
<EmojiPickerPrimitive.Root
className={cn(
"elevation relative isolate flex h-[368px] w-fit flex-col overflow-hidden rounded-xl bg-white shadow-elevation after:pointer-events-none after:absolute after:inset-0 after:z-10 after:rounded-[inherit] dark:bg-neutral-900 dark:after:shadow-[inset_0_0_0_1px_var(--color-neutral-800)]",
className,
)}
columns={columns}
{...props}
>
<EmojiPickerPrimitive.Search className="focusable z-10 mx-2 mt-2 appearance-none rounded-md bg-neutral-100 px-2.5 py-2 text-sm dark:bg-neutral-800" />
<EmojiPickerPrimitive.Viewport className="scrollbar-track-[transparent] scrollbar-thumb-neutral-500/30 dark:scrollbar-thumb-neutral-400/30 relative flex-1 outline-hidden">
<EmojiPickerPrimitive.Loading className="absolute inset-0 flex items-center justify-center text-neutral-400 text-sm dark:text-neutral-500">
Loading…
</EmojiPickerPrimitive.Loading>
<EmojiPickerPrimitive.Empty className="absolute inset-0 flex items-center justify-center text-neutral-400 text-sm dark:text-neutral-500">
No emoji found.
</EmojiPickerPrimitive.Empty>
<EmojiPickerPrimitive.List
className="select-none pb-1.5"
components={{
Emoji: ({ emoji, ...props }) => (
<button
className="flex size-8 items-center justify-center whitespace-nowrap rounded-md text-lg data-[active]:bg-neutral-100 dark:data-[active]:bg-neutral-800"
{...props}
>
{emoji.emoji}
</button>
),
Row: ({ children, ...props }) => (
<div className="scroll-my-1.5 px-1.5" {...props}>
{children}
</div>
),
CategoryHeader: ({ category, ...props }) => (
<div
className="after:-top-1 relative bg-white px-3 pt-3 pb-1.5 font-medium text-neutral-600 text-xs after:absolute after:inset-x-0 after:h-2 after:bg-white dark:bg-neutral-900 dark:text-neutral-400 dark:after:bg-neutral-900"
{...props}
>
{category.label}
</div>
),
}}
/>
</EmojiPickerPrimitive.Viewport>
</EmojiPickerPrimitive.Root>
);
}

export function CustomEmojiDataPreview() {
const [locale, setLocale] = useState("tr");

return (
<ExamplePreview className="h-[480px] flex-col gap-3">
<div className="flex gap-1.5">
{LOCALES.map((option) => (
<Button
key={option.locale}
onClick={() => setLocale(option.locale)}
size="sm"
variant={option.locale === locale ? "secondary" : "ghost"}
>
{option.label}
</Button>
))}
</div>
<EmojiPicker
locale={locale}
onEmojiSelect={(emoji) => {
toast(emoji);
}}
resolveEmojiData={resolveEmojiData}
/>
</ExamplePreview>
);
}
Loading