Nasaq

MentionTextarea

Textarea that suggests people at the caret when you type @ (or another trigger) and reports the text plus a mentions array.

PreviewOpen ↗

Code

import { Field, FieldLabel, type Mention, MentionTextarea } from "@fadymondy/nasaq/web";import { useState } from "react";const people = [  { id: "u1", name: "Sara Ali", description: "Design lead" },  { id: "u2", name: "Omar Nasser", description: "Engineer" },];export function Comment() {  const [mentions, setMentions] = useState<Mention[]>([]);  return (    <Field>      <FieldLabel>Comment</FieldLabel>      <MentionTextarea        rows={4}        suggestions={people}        placeholder="Type @ to mention someone"        onValueChange={(text, next) => setMentions(next)}      />      <output>{mentions.length} mentioned</output>    </Field>  );}

Collaboration · beta

Live examples and controls: MentionTextarea in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/mention-textarea.json

A Textarea that opens a list of suggestions at the caret when the user types a trigger character (@ by default). Choosing an entry inserts @name. onValueChange returns the text and a mentions array with the id and the range of each mention, so the server can notify the right people.

When to use

  • Comments, notes and messages where people are referenced.

When not to use

  • Picking a value from a list in a single-line control: use combobox.
  • Free tags: use tag-input.

Import

import { MentionTextarea } from "@fadymondy/nasaq/web";

Quick start

import { Field, FieldLabel, type Mention, MentionTextarea } from "@fadymondy/nasaq/web";
import { useState } from "react";

const people = [
  { id: "u1", name: "Sara Ali", description: "Design lead" },
  { id: "u2", name: "Omar Nasser", description: "Engineer" },
];

export function Comment() {
  const [mentions, setMentions] = useState<Mention[]>([]);
  return (
    <Field>
      <FieldLabel>Comment</FieldLabel>
      <MentionTextarea
        rows={4}
        suggestions={people}
        placeholder="Type @ to mention someone"
        onValueChange={(text, next) => setMentions(next)}
      />
      <output>{mentions.length} mentioned</output>
    </Field>
  );
}

Anatomy

div                 data-slot="mention-textarea"   (relative wrapper)
  textarea          data-slot="textarea"           (role="combobox")
  ul                data-slot="mention-list"       (role="listbox", at the caret)
    li              data-slot="mention-option"     (role="option", data-active)
  div               data-slot="mention-empty"      (when nothing matches)

API

PropTypeDefaultDescription
suggestionsMentionOption[]required{ id, name, description?, avatar?, kind?, handle?, presence?, keywords? }. kind is person (default), team or group: the list is sectioned by kind, teams and groups show an icon, people a presence dot. handle and keywords are searchable.
value / defaultValuestring""Controlled or initial text.
onValueChange(value: string, mentions: Mention[]) => voidText and every mention still in it, in text order.
defaultMentionsMention[][]Mentions that are already in value/defaultValue. Entries whose text no longer reads @name are dropped.
triggerstring"@"The character (or string) that opens the list. Must start a word.
maxSuggestionsnumber8Most rows shown.
listLabelstring"Mentions" / "الإشارات"Listbox name.
emptyLabelstring"No matches" / "لا نتائج"Shown when the query matches nobody.
wrapperClassNamestringClass of the wrapper. className goes on the textarea.

Other props go to the textarea (rows, placeholder, disabled, ...).

Mention = { id: string; name: string; start: number; end: number }; text.slice(start, end) is @name. Ranges follow the text as the user edits before them, and a mention is dropped when the user edits inside it.

Examples

Arabic, with a hash trigger:

import { MentionTextarea } from "@fadymondy/nasaq/web";

export function Arabic() {
  return (
    <MentionTextarea
      rows={3}
      trigger="#"
      suggestions={[
        { id: "t1", name: "التصميم" },
        { id: "t2", name: "الفوترة" },
      ]}
      placeholder="اكتب # لإضافة وسم"
    />
  );
}

Accessibility

The textarea is an ARIA combobox (aria-autocomplete="list", aria-expanded, aria-controls, aria-activedescendant); the list is a listbox of options. Focus never leaves the textarea. A polite status region announces the number of suggestions.

KeyAction
Arrow Down / UpMoves the highlight (wraps).
Enter / TabInserts the highlighted mention.
EscapeCloses the list until the next trigger.
Any other keyTypes; the list filters as you type.

Without a match, or after Escape, Enter and Tab behave as in a normal textarea.

RTL & i18n

Filtering uses normalizeForSearch, so case, Arabic diacritics, tatweel and alef/yeh/teh-marbuta variants are ignored. The list opens at the trigger's position, measured from the textarea's own direction, so it works right-to-left. A space is inserted after the name (not part of the mention). Default strings are in English and Arabic; localise listLabel and emptyLabel for other languages.

Styling & tokens

Uses bg-popover, border-border, shadow-floating, bg-nq-selected (highlighted row), text-muted-foreground. Target data-active on options. The list is positioned inside the wrapper; it does not flip above the field near the bottom of the viewport.

Do / Don't

  • Do keep suggestions short or filter them on the server and pass the result.
  • Do send mentions (ids) to the server rather than parsing the text.
  • Don't rely on the range for anything after the user edits without your onValueChange seeing it.

Lab

https://docs.nasaqui.com/?path=/docs/components-collaboration-mention-textarea--docs

On this page