Inputs
Emoji Picker
The emoji keyboard: search, categories, skin tone and a memory of what you picked last.
import { EmojiPicker } from 'omaris' 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
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.
<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
<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
<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.
EmojiButtonis that shape ready-made. - A "pick an icon" field for a status, a channel or a project. Pass
groupswith 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
groupsfor 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
EmojiButtononly for "more".
Do
- Drop
EmojiPickerinto a container and let it size itself. The column count comes from the width. - Use
EmojiButtonover a hand-rolled popover. It is already a bottom sheet on a phone and anchored on a pointer. - Bind
skinTonewhen the rest of the app shows people's emoji. - Give
Emojialabelwhen the emoji is the whole message ("🟢" for "online"). Leave it off when text beside it already says so.
Don't
- Set
autofocuson a picker visible on page load. It steals the cursor. - Share a
storageKeybetween unrelated pickers, or recents leak between them.nullkeeps 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 EmojiPickersmmd(default)lg
Cell size, and with it the emoji, the panel width and the rhythm.
size Emojixssmmd(default)lgxl2xlinherit
size EmojiButtonsmmd(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".
emptyTextemptyTextitself.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 bindableDefaults 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 smmdlg
groups EmojiGroup[] Your own set, in place of the built-in one.
search Defaults to true
boolean Show the search field.
query bindableDefaults 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 bindableDefaults to 0
EmojiSkinTone The chosen skin tone. Bindable.
recent bindableDefaults 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 xssmmdlgxl2xlinherit- 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 bindableDefaults to ''
string The emoji last chosen, tone applied. Bindable.
open bindableDefaults to false
boolean Whether the panel is showing. Bindable.
onselect (emoji: string, entry: EmojiEntry) => void size Defaults to 'md'
EmojiButtonSize smmdlg
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 bindableDefaults to 0
EmojiSkinTone The chosen skin tone. Bindable, and remembered with the recents.
recent bindableDefaults 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.