ReactionEmojiPicker

free

ReactionEmojiPicker floats a compact emoji grid from a trigger so users can add reactions without leaving the current context.

@floating-ui/reactreact-icons
Preview
Dark

Click the smiley to pick a reaction

import { useState } from 'react';
import ReactionEmojiPicker from '@/components/composites/ReactionEmojiPicker';

export default function UsageDemo() {
  const [picked, setPicked] = useState<string | null>(null);

  return (
    <div className="flex flex-col items-center gap-4">
      <ReactionEmojiPicker onSelect={setPicked} />
      <p className="text-sm text-muted-foreground">
        {picked ? (
          <>Selected: <span className="text-base">{picked}</span></>
        ) : (
          'Click the smiley to pick a reaction'
        )}
      </p>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add ReactionEmojiPicker

Examples

Custom Trigger

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import ReactionEmojiPicker from '@/components/composites/ReactionEmojiPicker';
import { PiSmiley } from 'react-icons/pi'

export default function CustomTriggerDemo() {
  const [picked, setPicked] = useState<string | null>(null);

  return (
    <ReactionEmojiPicker
      trigger={
        <Button variant="default" icon={<PiSmiley />}>
          Add reaction
        </Button>
      }
      onSelect={setPicked}
    />
  );
}

Custom Emojis

Preview
Dark
import { useState } from 'react';
import ReactionEmojiPicker from '@/components/composites/ReactionEmojiPicker';

const FOOD_EMOJIS = [
  '🍕', '🍔', '🌮', '🍣', '🍜',
  '🍩', '🧁', '🍦', '🍎', '🥑',
] as const;

export default function CustomEmojisDemo() {
  const [picked, setPicked] = useState<string | null>(null);

  return (
    <ReactionEmojiPicker
      emojis={FOOD_EMOJIS}
      onSelect={setPicked}
    />
  );
}

Placement

Preview
Dark
import Button from '@/components/ui/Button';
import ReactionEmojiPicker from '@/components/composites/ReactionEmojiPicker';

const placements = ['top', 'bottom', 'right'] as const;

export default function PlacementDemo() {
  return (
    <div className="flex items-center justify-center gap-4">
      {placements.map((placement) => (
        <ReactionEmojiPicker
          key={placement}
          placement={placement}
          trigger={
            <Button type="button">
              {placement}
            </Button>
          }
        />
      ))}
    </div>
  );
}

Reaction Bar

Preview
Dark
import { useState } from 'react';
import ReactionEmojiPicker from '@/components/composites/ReactionEmojiPicker';

type Reaction = { emoji: string; count: number; reacted: boolean };

const INITIAL: Reaction[] = [
  { emoji: '👍', count: 4, reacted: false },
  { emoji: '❤️', count: 2, reacted: true },
];

export default function ReactionBarDemo() {
  const [reactions, setReactions] = useState<Reaction[]>(INITIAL);

  const toggle = (emoji: string) => {
    setReactions((prev) => {
      const current = prev.find((r) => r.reacted);
      const target = prev.find((r) => r.emoji === emoji);

      if (current?.emoji === emoji) {
        return prev.map((r) =>
          r.emoji === emoji
            ? { ...r, count: r.count - 1, reacted: false }
            : r,
        );
      }

      const next = prev.map((r) => {
        if (r.emoji === current?.emoji) {
          return { ...r, count: r.count - 1, reacted: false };
        }
        if (r.emoji === emoji) {
          return { ...r, count: r.count + 1, reacted: true };
        }
        return r;
      });

      if (!target) {
        return [...next, { emoji, count: 1, reacted: true }];
      }

      return next;
    });
  };

  const visible = reactions.filter((r) => r.count > 0);

  return (
    <div className="flex flex-wrap items-center gap-2">
      {visible.map((r) => (
        <button
          key={r.emoji}
          type="button"
          onClick={() => toggle(r.emoji)}
          className={[
            'flex items-center gap-1 rounded-full border px-2 py-0.5 text-sm',
            r.reacted
              ? 'border-primary bg-primary-soft text-primary'
              : 'border-border hover:bg-accent',
          ].join(' ')}
        >
          <span>{r.emoji}</span>
          <span className="font-medium tabular-nums">{r.count}</span>
        </button>
      ))}
      <ReactionEmojiPicker onSelect={toggle} placement="top" />
    </div>
  );
}

API

PropDescriptionTypeDefault
emojisThe emoji set rendered in the picker grid.readonly string[]['👍','❤️','😂','😮','😢','😡','👏','🎉']
onSelectCalled with the chosen emoji string. The picker closes immediately after.(emoji: string) => void
triggerCustom trigger element. Defaults to a smiley icon button.ReactNode
placementPopover panel placement relative to the trigger.PopoverPlacement'top'
widthPixel width of the popover panel.number250

Types

type PopoverPlacement =
  | 'top' | 'top-start' | 'top-end'
  | 'bottom' | 'bottom-start' | 'bottom-end'
  | 'left' | 'left-start' | 'left-end'
  | 'right' | 'right-start' | 'right-end'