Chat composer
A message input with shortcode autocomplete and an emoji picker button
A chat composer combines an autocomplete-driven input with a picker button. Typing : surfaces inline :shortcode suggestions via EmoteAutocomplete, the ReactionButton.Popover opens a full EmoteList, and Enter sends — pushing each message onto a list rendered as bubbles above the input.
The pattern
import { EmoteAutocomplete, EmoteList, ReactionButton } from '@emoteer/react';
import { useState } from 'react';
export function ChatComposer() {
const [value, setValue] = useState('');
const [pickerOpen, setPickerOpen] = useState(false);
const [messages, setMessages] = useState<string[]>([]);
const send = () => {
const text = value.trim();
if (!text) return;
setMessages((prev) => [...prev, text]);
setValue('');
};
return (
<div className="flex flex-col gap-3">
{messages.length > 0 && (
<div className="flex flex-col items-end gap-1.5">
{messages.map((message, i) => (
<div
key={i}
className="max-w-[80%] rounded-2xl rounded-br-sm bg-foreground px-3 py-1.5 text-sm text-background"
>
{message}
</div>
))}
</div>
)}
<div className="flex items-center gap-1 rounded-2xl border p-1 focus-within:border-primary">
<EmoteAutocomplete.Root onSelect={(_, next) => setValue(next)}>
<EmoteAutocomplete.Input
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
// The autocomplete consumes Enter to pick a highlighted
// suggestion; only send when it didn't.
if (e.key === 'Enter' && !e.defaultPrevented) {
e.preventDefault();
send();
}
}}
placeholder="Message… type : for emoji"
className="min-w-0 flex-1 border-0 bg-transparent px-3 py-1.5 outline-none"
/>
<EmoteAutocomplete.Content>
<EmoteAutocomplete.List />
</EmoteAutocomplete.Content>
</EmoteAutocomplete.Root>
<ReactionButton.Popover
open={pickerOpen}
onOpenChange={(d) => setPickerOpen(d.open)}
>
<ReactionButton.Trigger>
<ReactionButton.Plus />
</ReactionButton.Trigger>
<ReactionButton.Content>
<EmoteList.Root
onSelect={(e) => {
setValue((prev) => `${prev}${e.unicode}`);
setPickerOpen(false);
}}
>
<EmoteList.Search />
<EmoteList.Tabs />
<EmoteList.Grid />
</EmoteList.Root>
</ReactionButton.Content>
</ReactionButton.Popover>
<button
type="button"
onClick={send}
className="rounded-full bg-foreground px-4 py-1.5 text-sm font-semibold text-background"
>
Send
</button>
</div>
</div>
);
}<script lang="ts">
import { EmoteAutocomplete, EmoteList, ReactionButton } from '@emoteer/svelte';
let value = $state('');
let pickerOpen = $state(false);
let messages = $state<string[]>([]);
function send() {
const text = value.trim();
if (!text) return;
messages = [...messages, text];
value = '';
}
</script>
<div class="flex flex-col gap-3">
{#if messages.length > 0}
<div class="flex flex-col items-end gap-1.5">
{#each messages as message}
<div
class="max-w-[80%] rounded-2xl rounded-br-sm bg-foreground px-3 py-1.5 text-sm text-background"
>
{message}
</div>
{/each}
</div>
{/if}
<div class="flex items-center gap-1 rounded-2xl border p-1 focus-within:border-primary">
<EmoteAutocomplete.Root onSelect={(_e, next) => (value = next)}>
<EmoteAutocomplete.Input
{value}
oninput={(e) => (value = e.currentTarget.value)}
onkeydown={(e) => {
// The autocomplete consumes Enter to pick a highlighted
// suggestion; only send when it didn't.
if (e.key === 'Enter' && !e.defaultPrevented) {
e.preventDefault();
send();
}
}}
placeholder="Message… type : for emoji"
class="min-w-0 flex-1 border-0 bg-transparent px-3 py-1.5 outline-none"
/>
<EmoteAutocomplete.Content>
<EmoteAutocomplete.List />
</EmoteAutocomplete.Content>
</EmoteAutocomplete.Root>
<ReactionButton.Popover open={pickerOpen} onOpenChange={(d) => (pickerOpen = d.open)}>
<ReactionButton.Trigger />
<ReactionButton.Content>
<EmoteList.Root
onSelect={(e) => {
value += e.unicode;
pickerOpen = false;
}}
>
<EmoteList.Search />
<EmoteList.Tabs />
<EmoteList.Grid />
</EmoteList.Root>
</ReactionButton.Content>
</ReactionButton.Popover>
<button
type="button"
onclick={send}
class="rounded-full bg-foreground px-4 py-1.5 text-sm font-semibold text-background"
>
Send
</button>
</div>
</div>What it does
- Typing
:followed by a shortcode (e.g.:grin) surfaces inline suggestions. Arrow keys move through them; Enter or click picks one. - The picker button opens a full
EmoteList; selecting an emoji appends it to the message. - Enter sends — except while a suggestion is highlighted, where the autocomplete claims the key (so it never sends mid-completion). Each sent message is pushed onto the local
messageslist and rendered as a bubble.
Notes
- The input is controlled here so a single
valuedrives the autocomplete, the picker and the send button.EmoteAutocomplete.Inputis uncontrolled by design but acceptsvalue/onChangefor this. messagesis local component state for the demo — swap it for your own send handler or store to persist and stream real messages.- Want a growing multi-line box? Swap the single input for
EmoteTextAreato also auto-expand:shortcode:to unicode as the user types.