Skip to content
omaris

Inputs

Emoji Picker

The emoji keyboard: search, categories, skin tone and a memory of what you picked last.

import { EmojiPicker } from 'omaris'
Learn

Examples

Basic

The panel on its own: type to search, arrow around the grid, Enter to take one. It fills whatever width it is given.

Smileys & emotion

People & body

Animals & nature

Food & drink

Activity

Travel & places

Objects

Symbols

Flags

Pick an emoji

Picked: nothing yet

<script lang="ts">	import { EmojiPicker, Emoji } from 'omaris';​	let picked = $state('');</script>​<div class="flex w-full max-w-sm flex-col gap-3">	<EmojiPicker bind:value={picked} storageKey={null} />	<p class="text-body-sm text-muted-foreground">		Picked:		{#if picked}			<Emoji emoji={picked} size="lg" label={picked} />		{:else}			nothing yet		{/if}	</p></div>

In a composer

The shape it takes in a message box: EmojiButton is the panel behind a trigger — anchored on a pointer, a bottom sheet on a phone.

<script lang="ts">	import { EmojiButton, Input } from 'omaris';​	let message = $state('Ship it ');</script>​<div class="flex w-full max-w-sm items-end gap-2">	<Input class="flex-1" bind:value={message} aria-label="Message" />	<EmojiButton storageKey={null} onselect={(emoji) => (message += emoji)} /></div>

Custom trigger

A reaction bar. The trigger is a snippet, so it can be anything; the panel is the same one.

Reacted with 👍
<script lang="ts">	import { EmojiButton, Emoji } from 'omaris';​	const QUICK = ['👍', '❤️', '😂', '🎉'];	let reaction = $state('👍');</script>​<div class="flex items-center gap-1.5">	{#each QUICK as emoji (emoji)}		<button			type="button"			class="grid size-9 cursor-pointer place-items-center rounded-full bg-surface-container transition-[scale,background-color] duration-150 ease-spring hover:scale-110 hover:bg-surface-container-high active:scale-95 motion-reduce:transition-none"			onclick={() => (reaction = emoji)}			aria-label="React with {emoji}"		>			<Emoji {emoji} size="lg" />		</button>	{/each}​	<EmojiButton storageKey={null} bind:value={reaction} picker={{ height: 220, tones: false }}>		{#snippet trigger({ open })}			<span				class="grid size-9 cursor-pointer place-items-center rounded-full border border-dashed border-border text-muted-foreground transition-[background-color,rotate] duration-200 ease-emphasized hover:bg-surface-container motion-reduce:transition-none"				class:rotate-45={open}			>				<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-4">					<path d="M12 5v14M5 12h14" stroke-linecap="round" />				</svg>			</span>		{/snippet}	</EmojiButton>​	<span class="ms-3 text-body-sm text-muted-foreground">		Reacted with <Emoji emoji={reaction} size="lg" label={reaction} />	</span></div>

Skin tone

Skin tone is chosen once, in the preview row, and applies to every emoji that takes one. Bind it when the rest of the app needs to know.

Smileys & emotion

People & body

Animals & nature

Food & drink

Activity

Travel & places

Objects

Symbols

Flags

Pick an emoji
Tone 3
<script lang="ts">	import { EmojiPicker, Emoji, type EmojiSkinTone } from 'omaris';​	let tone = $state<EmojiSkinTone>(3);</script>​<div class="flex w-full max-w-md flex-wrap items-start gap-6">	<EmojiPicker bind:skinTone={tone} storageKey={null} height={220} class="min-w-64 flex-1" />	<div class="flex flex-col gap-2">		<span class="text-body-sm text-muted-foreground">Tone {tone}</span>		<div class="flex gap-2">			<Emoji emoji="👋" {tone} size="xl" />			<Emoji emoji="👍" {tone} size="xl" />			<Emoji emoji="🙏" {tone} size="xl" />			<Emoji emoji="🧑‍💻" {tone} size="xl" />		</div>	</div></div>

Emoji

Emoji on its own: the emoji font first, sat on the x-height so it keeps a line's rhythm, and a label only when it carries meaning.

Deployed to production — the emoji is decoration next to text that already says so, so it has no label. A status of its own does: 🟢.

<script lang="ts">	import { Emoji, Text } from 'omaris';</script>​<div class="flex flex-col gap-4">	<div class="flex items-end gap-3">		<Emoji emoji="🎉" size="xs" />		<Emoji emoji="🎉" size="sm" />		<Emoji emoji="🎉" size="md" />		<Emoji emoji="🎉" size="lg" />		<Emoji emoji="🎉" size="xl" />		<Emoji emoji="🎉" size="2xl" />	</div>	<Text class="max-w-md">		Deployed to production <Emoji emoji="🚀" /> — the emoji is decoration next to text that already says		so, so it has no label. A status of its own does: <Emoji emoji="🟢" label="Online" />.	</Text></div>

Overridden

The overridden case: a set of your own, no chrome, a fixed column count, and classes on the heading and the cells.

Status

Smileys & emotion

People & body

Animals & nature

Food & drink

Activity

Travel & places

Objects

Symbols

Flags

Pick an emoji
<script lang="ts">	import { EmojiPicker, type EmojiGroup } from 'omaris';​	/** A status picker, in place of the built-in set. */	const STATUS: EmojiGroup[] = [		{			id: 'status',			label: 'Status',			icon: '🟢',			emojis: [				{ emoji: '🟢', name: 'available', keywords: ['free'], tone: false, category: 'symbols' },				{ emoji: '🟡', name: 'busy', keywords: ['later'], tone: false, category: 'symbols' },				{					emoji: '🔴',					name: 'do not disturb',					keywords: ['dnd'],					tone: false,					category: 'symbols'				},				{ emoji: '🌴', name: 'on holiday', keywords: ['away'], tone: false, category: 'nature' },				{					emoji: '🏠',					name: 'working from home',					keywords: ['wfh'],					tone: false,					category: 'travel'				},				{ emoji: '🎧', name: 'heads down', keywords: ['focus'], tone: false, category: 'objects' }			]		}	];</script>​<div class="flex w-full flex-wrap items-start gap-6">	<div class="w-56 max-w-full rounded-shape-lg border border-border bg-surface-container-low p-2">		<EmojiPicker			bare			groups={STATUS}			categories={false}			preview={false}			tones={false}			columns={3}			height={140}			searchPlaceholder="Search status"			storageKey={null}			class="w-full"		/>	</div>	<EmojiPicker		storageKey={null}		columns={10}		height="12rem"		class="w-full max-w-sm"		classes={{ heading: 'text-primary', cell: 'rounded-full' }}	/></div>

When to use it

Use it for

  • A message composer, a comment box, a reaction bar: anywhere people add an emoji to text. EmojiButton is that shape ready-made.
  • A "pick an icon" field for a status, a channel or a project. Pass groups with your own set and it picks from exactly those.
  • One emoji inline in copy, at a named size, with a screen-reader label only when it means something. That is Emoji.

Not for

  • A picker that must cover every Unicode emoji. The built-in set is a few hundred; pass groups for the long tail.
  • A list of named options where the emoji is decoration → a Select or Combobox with an emoji in each row.
  • A colour, a date, or an icon from your icon set. It is an emoji keyboard.
  • Six fixed reactions like "react with 👍" → six plain buttons, with EmojiButton only for "more".

Do

  • Drop EmojiPicker into a container and let it size itself. The column count comes from the width.
  • Use EmojiButton over a hand-rolled popover. It is already a bottom sheet on a phone and anchored on a pointer.
  • Bind skinTone when the rest of the app shows people's emoji.
  • Give Emoji a label when the emoji is the whole message ("🟢" for "online"). Leave it off when text beside it already says so.

Don't

  • Set autofocus on a picker visible on page load. It steals the cursor.
  • Share a storageKey between unrelated pickers, or recents leak between them. null keeps recents for the life of the component only.
  • Put a bare emoji in a <span> with a size class. It hangs below the baseline and is read out by its Unicode name.

Quick reference

size EmojiPicker
  • sm
  • md (default)
  • lg

Cell size, and with it the emoji, the panel width and the rhythm.

size Emoji
  • xs
  • sm
  • md (default)
  • lg
  • xl
  • 2xl
  • inherit
size EmojiButton
  • sm
  • md (default)
  • lg

API

EmojiPicker

The emoji keyboard: search, categories, skin tone and a memory of what you picked last.

It is a panel, not a popup — drop it into a sheet, a dialog, a card or the side of a composer, and it fills whatever it is given. The grid columns come from the container's width rather than a breakpoint, so the same picker is nine across in a desktop popover and six across on a phone without being told which one it is in. EmojiButton is this panel plus a trigger, anchored on a pointer and a bottom sheet on a touch screen.

The whole surface is reachable from the keyboard: type to search, arrow around the grid (the arrows wrap across rows and carry on into the next category), Enter to take one, Escape to clear the search. Focus moves by a roving tabindex, so Tab passes over the grid in one step rather than walking five hundred buttons.

import { EmojiPicker } from 'omaris'
<EmojiPicker onselect={(emoji) => (message += emoji)} /><EmojiPicker bind:value bind:skinTone height={280} />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The panel.
header
The search row.
search
The field itself — a self-contained pill, not the Input frame.
searchIcon
The magnifier in the search field.
input
The search field's own <input>.
clear
The × that empties the field. Present only while there is one.
tabs
The category rail.
indicator
The pill that slides between tabs. One element that moves, rather than a background that fades in and out on nine of them.
tab
One category, drawn as its own representative emoji.
scroller
The scrolling body.
section
One category's heading and grid.
heading
The category name, which sticks to the top as you scroll past it.
grid
One category's cells — as many across as the width holds.
cell
One emoji.
empty
"No emoji found".
emptyIcon
The face over "no emoji found".
emptyText
emptyText itself.
footer
The preview row along the bottom.
previewEmoji
The emoji under the cursor, shown large.
previewName
Its name.
tones
The skin tone control: one swatch that unfurls into six.
toneToggle
The swatch that opens the tone row, showing the current tone.
toneList
The row of six, which grows out of the toggle rather than appearing.
tone
One tone in that row.

Props

value bindable

Defaults to ''

string

The emoji last chosen, tone applied. Bindable.

onselect
(emoji: string, entry: EmojiEntry) => void

Called with the chosen emoji and the entry behind it.

onclose
() => void

Escape on an empty search field.

size

Defaults to 'md'

EmojiPickerSize
sm
md
lg
groups
EmojiGroup[]

Your own set, in place of the built-in one.

search

Defaults to true

boolean

Show the search field.

query bindable

Defaults to ''

string

What the search field is bound to. Bindable.

searchPlaceholder

Defaults to 'Search emoji'

string
categories

Defaults to true

boolean

Show the category rail.

preview

Defaults to true

boolean

Show the preview row along the bottom.

tones

Defaults to true

boolean

Offer the skin tone control. Needs preview.

skinTone bindable

Defaults to 0

EmojiSkinTone

The chosen skin tone. Bindable.

recent bindable

Defaults to []

string[]

Recently used emoji, most recent first. Bindable.

storageKey

Defaults to 'omaris:emoji'

string | null

localStorage key the recents and the tone are remembered under. null keeps them for the life of the component only.

maxRecent

Defaults to 24

number

How many recents to keep.

columns
number

Force a column count instead of fitting as many as the width allows.

height

Defaults to 260

number | string

Height of the scrolling area — a number of px or any CSS length.

autofocus

Defaults to false

boolean

Put the cursor in the search field on mount.

bare

Defaults to false

boolean

Drop the panel's own border, radius and background.

emptyText

Defaults to 'No emoji found'

string
footer
Snippet<[{ emoji: string; name: string }]>

Replaces the preview row's contents, keeping the row.

class
string
classes
EmojiPickerClasses

Per-part Tailwind overrides. class still covers the root.

Emoji

One emoji, drawn by the emoji font at a size you can name.

A bare emoji in a <span> is drawn by whichever family in the text stack happens to cover the character, sits on the text baseline (so it hangs below a line of copy) and is read out by a screen reader as its Unicode name whether that helps or not. This fixes all three: the emoji font first, optical alignment, and a label that is yours — or nothing at all when the emoji is decoration next to text that already says it.

import { Emoji } from 'omaris'
<Emoji emoji="🎉" size="xl" label="Celebrating" /><Emoji emoji="👋" tone={3} />

Props

emoji required
string

The character.

label
string

What it means, for a screen reader — "Celebrating", not "party popper". Leave it out and the emoji is hidden from assistive tech, which is right whenever the text beside it already says the same thing.

tone

Defaults to 0

EmojiSkinTone

Tint it. 0 is the default yellow.

size

Defaults to 'md'

EmojiSize
xs
sm
md
lg
xl
2xl
inherit
Takes `--emoji-size` from whatever it sits in.
class
string

EmojiButton

The emoji keyboard behind a button — the shape it takes in a composer, a reaction bar or a form field.

It presents itself two ways from one set of props, because a popover and a phone do not mix: on a pointer it is a panel anchored to the trigger, flipping to whichever side has room; below mobileQuery it is a bottom sheet you can throw away with a flick, with the grid sized for a thumb. Neither is a second component to keep in step — the same EmojiPicker is inside both.

The trigger is a snippet, so it can be anything — the default is a round button showing the emoji you last chose.

import { EmojiButton } from 'omaris'
<EmojiButton bind:value /><EmojiButton onselect={(emoji) => react(emoji)} placeholder="🙂" />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

trigger
The default trigger.
popover
The positioned box, on a pointer. Nothing but position: the anchor measures this element, and a transform on it would be measured too — which is how a panel ends up half off the screen mid-entrance.
surface
The panel inside it, which is what actually animates in.
sheetPicker
The picker inside the bottom sheet.

Props

value bindable

Defaults to ''

string

The emoji last chosen, tone applied. Bindable.

open bindable

Defaults to false

boolean

Whether the panel is showing. Bindable.

onselect
(emoji: string, entry: EmojiEntry) => void
size

Defaults to 'md'

EmojiButtonSize
sm
md
lg
placeholder

Defaults to '\u{1F642}'

string

Shown on the default trigger before anything is chosen.

reflect

Defaults to true

boolean

Put the chosen emoji on the trigger. On by default.

closeOnSelect

Defaults to true

boolean

Close as soon as one is picked. On by default.

label

Defaults to 'Pick an emoji'

string

Accessible name for the default trigger.

side

Defaults to 'bottom'

AnchorSide

Preferred side of the trigger. Flips when there isn't room.

align

Defaults to 'start'

AnchorAlign
offset

Defaults to 8

number

Gap between trigger and panel, in px.

responsive

Defaults to true

boolean

Become a bottom sheet on a narrow screen. On by default.

mobileQuery

Defaults to '(max-width: 639px)'

string
skinTone bindable

Defaults to 0

EmojiSkinTone

The chosen skin tone. Bindable, and remembered with the recents.

recent bindable

Defaults to []

string[]

Recently used emoji, most recent first. Bindable.

storageKey

Defaults to 'omaris:emoji'

string | null

localStorage key for the recents and the tone. null to not persist.

picker
EmojiPickerOptions

Everything else the panel takes.

trigger
Snippet<[{ open: boolean; emoji: string }]>

Your own trigger, in place of the round button. Draw the shape only — it is rendered inside the button that opens the panel, so it must not contain an interactive element of its own.

class
string
classes
EmojiButtonClasses

Per-part Tailwind overrides. class still covers the trigger.