Post reactions
A Discord-style message with a reaction row and an add-reaction picker
A chat-style message with reactions: the avatar, author and body sit above the reaction row. Existing reactions show their counts and active state, an "add" button opens a picker, and clicking a chip toggles your reaction.
vyersToday at 12:42
Shipping the Svelte bindings today š ā react with whatever fits.
The pattern
import {
EmoteList,
ReactionButton,
ReactionCounter,
type Reaction,
} from '@emoteer/react';
import { useState } from 'react';
const INITIAL: Reaction[] = [
{ emoji: 'š', count: 8, active: false },
{ emoji: 'ā¤ļø', count: 3, active: true },
{ emoji: 'š', count: 2, active: false },
];
export function Message() {
const [reactions, setReactions] = useState(INITIAL);
const [pickerOpen, setPickerOpen] = useState(false);
const handleToggle = (emoji: string, active: boolean) => {
setReactions((prev) =>
prev
.map((r) =>
r.emoji === emoji
? { ...r, active, count: r.count + (active ? 1 : -1) }
: r,
)
.filter((r) => r.count > 0),
);
};
const handleAdd = (emoji: string) => {
setReactions((prev) =>
prev.some((r) => r.emoji === emoji)
? prev
: [...prev, { emoji, count: 1, active: true }],
);
setPickerOpen(false);
};
return (
<div className="flex gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-lg">
š¦
</div>
<div className="flex flex-col gap-1.5">
<p className="text-sm">
<span className="font-semibold">vyers</span>{' '}
<span className="text-xs text-muted-foreground">Today at 12:42</span>
</p>
<p className="text-sm">
Shipping the Svelte bindings today š ā react with whatever fits.
</p>
<div className="mt-0.5 flex flex-wrap items-center gap-2">
<ReactionCounter reactions={reactions} onToggle={handleToggle} />
<ReactionButton.Popover
open={pickerOpen}
onOpenChange={(d) => setPickerOpen(d.open)}
>
<ReactionButton.Trigger>
<button className="h-7 rounded-full border px-2.5 text-sm text-muted-foreground">
ļ¼
</button>
</ReactionButton.Trigger>
<ReactionButton.Content>
<EmoteList.Root onSelect={(e) => handleAdd(e.unicode)}>
<EmoteList.Search />
<EmoteList.Tabs />
<EmoteList.Grid />
</EmoteList.Root>
</ReactionButton.Content>
</ReactionButton.Popover>
</div>
</div>
</div>
);
}<script lang="ts">
import {
EmoteList,
ReactionButton,
ReactionCounter,
type Reaction,
} from '@emoteer/svelte';
let reactions = $state<Reaction[]>([
{ emoji: 'š', count: 8, active: false },
{ emoji: 'ā¤ļø', count: 3, active: true },
{ emoji: 'š', count: 2, active: false },
]);
let pickerOpen = $state(false);
function handleToggle(emoji: string, active: boolean) {
reactions = reactions
.map((r) =>
r.emoji === emoji
? { ...r, active, count: r.count + (active ? 1 : -1) }
: r,
)
.filter((r) => r.count > 0);
}
function handleAdd(emoji: string) {
if (!reactions.some((r) => r.emoji === emoji)) {
reactions = [...reactions, { emoji, count: 1, active: true }];
}
pickerOpen = false;
}
</script>
<div class="flex gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-lg">
š¦
</div>
<div class="flex flex-col gap-1.5">
<p class="text-sm">
<span class="font-semibold">vyers</span>
<span class="text-xs text-muted-foreground">Today at 12:42</span>
</p>
<p class="text-sm">
Shipping the Svelte bindings today š ā react with whatever fits.
</p>
<div class="mt-0.5 flex flex-wrap items-center gap-2">
<ReactionCounter {reactions} onToggle={handleToggle} />
<ReactionButton.Popover open={pickerOpen} onOpenChange={(d) => (pickerOpen = d.open)}>
<ReactionButton.Trigger>ļ¼</ReactionButton.Trigger>
<ReactionButton.Content>
<EmoteList.Root onSelect={(e) => handleAdd(e.unicode)}>
<EmoteList.Search />
<EmoteList.Tabs />
<EmoteList.Grid />
</EmoteList.Root>
</ReactionButton.Content>
</ReactionButton.Popover>
</div>
</div>
</div>What it does
- The message chrome (avatar, author, timestamp, body) is plain markup ā the only stateful part is the reaction row.
- Toggling a chip bumps or decrements its count; reactions that hit
0drop off the list. - The add button opens a full picker; choosing an emoji pushes a new active reaction (or no-ops if it already exists).
Backend integration
The demo hardcodes the message and INITIAL reactions. In a real app, accept them as props and wrap handleToggle / handleAdd in mutations for optimistic updates ā roll back in onError, invalidate the message on success. The component stays the same; all async concerns live outside it.