Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Mention Input

Inputs

Mention Input React component for Tailwind CSS v4

Masked InputMonth Picker
Live props
State
defaultValue
error
required
disabled
readOnly
invalid
Behavior
allowCreateTags
maxLengthNone
suggestionsPlacement
submitShortcut
autoFocus
name
Layout
multiline
minRows3
maxRows8
Content
placeholder
label
description
Appearance
size
variant
radius
tone
color
Code
import { MentionInput } from "@/components/ui/mention-input"

<MentionInput />
  1. Home
  2. Components
  3. Inputs
  4. Mention Input
Pro

Mention Input React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React mention input where @ mentions people, # adds tags and any trigger you define inserts chips, stored as markup that survives renames.

Type @ at the start of a word and a list of people opens at the caret; keep typing (spaces allowed, so “Daniel Re” still filters, accents and case ignored) and the matching letters turn bold. Up and Down move through the list and wrap, PageUp and PageDown jump by five, Home and End go to the ends while the list is open, Enter or Tab inserts, a click or a tap inserts without the field losing focus. The mention lands as a tinted chip followed by a space, and the caret goes after it. Type # for tags: when nothing matches exactly the last row offers “Create #…”. Escape closes the list first; a second Escape reaches the dialog or sheet around the field. A mention is one piece: Backspace right after it or Delete right before it removes all of it, Left and Right step over it in one press, a click, a tap or Up / Down that would land inside it puts the caret at its nearer edge, a double-click or a drag that starts inside it selects all of it, and typing over a selection that cuts it removes it. Undo brings a removed chip back. Copying text with chips and pasting it into another mention field keeps the chips. ⌘ Enter (Ctrl Enter elsewhere) submits; the playground’s Save note adds the note below, rendered with MentionText. With maxLength a counter sits in the box and shakes when an insert would not fit. On a phone the text is 16px, rows are at least 44px and the list attaches to the edge of the box, flipping above it when the keyboard would cover it, so the keyboard never closes. Screen readers hear the plain text (“@Daniel Reed”), the highlighted suggestion as it moves, “No matches” when nothing fits, and a short note when a mention is added or removed.

Mention Input is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add mention-input, together with Field, Listbox, Avatar, Spinner, Text match and 7 more and the npm package motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add mention-input

Mention Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import MentionInput from @/components/ui/mention-input.

import { useState } from "react"
import { MentionInput, MentionText, getMentions, type MentionItem } from "@/components/ui/mention-input"

const team: MentionItem[] = [
  { id: "u-daniel", label: "Daniel Reed", avatar: "/demo/mihai-ionescu.jpg", description: "Sales manager" },
  { id: "u-sophia", label: "Sophia Lee", avatar: "/demo/ioana-dumitrescu.jpg", description: "Accounting" },
]
const tags: MentionItem[] = [
  { id: "t-follow-up", label: "follow-up" },
  { id: "t-quote", label: "quote" },
]

export function ClientNote() {
  const [note, setNote] = useState("Call @[Daniel Reed](u-daniel) tomorrow #[follow-up](t-follow-up)")
  const [saved, setSaved] = useState<string[]>([])
  return (
    <div className="flex flex-col gap-3">
      <MentionInput
        label="New note"
        value={note}
        onValueChange={setNote}
        people={team}
        tags={tags}
        placeholder="Write a note. @ for teammates, # for tags"
        onSubmit={(value) => {
          console.log("notify", getMentions(value).map((m) => m.id))
          setSaved((prev) => [value, ...prev])
          setNote("")
        }}
      />
      {saved.map((value, i) => (
        <MentionText key={i} value={value} className="text-sm" />
      ))}
    </div>
  )
}

Props

  • defaultValuedefault ""
    string

    The starting markup while uncontrolled. Mentions are trigger + [label](id): @[Daniel Reed](u-daniel), #[follow-up](t-follow-up); brackets and backslashes inside are escaped with \. The playground falls back to a sample note when it is empty.

  • value
    string

    The markup, to control it from outside; pair it with onValueChange. A change from outside while typing keeps the caret at the same plain-text offset.

  • onValueChange
    (value: string, context: MentionChangeContext) => void

    Called with the new markup on every change, plus a context { text; mentions: MentionRef[] }: the plain text people see and the mentions with their offsets (for notifications).

  • onChange
    (event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void

    The native change event of the control, untouched. Its value is the plain text, not the markup.

  • people
    MentionItem[]

    Shorthand for the @ trigger: { id; label; avatar?; description?; icon?; keywords?; disabled? }. Rows show the avatar (initials without a photo) and the description under the name; spaces are allowed in the query.

  • tags
    MentionItem[]

    Shorthand for the # trigger. Rows show a # tile unless an item has its own icon.

  • triggers
    MentionTrigger[]

    Full control: { char; items?; onSearch?(query) => Promise<MentionItem[]>; allowCreate?; onCreate?(label); tone?; color?; title?; minChars?; maxResults? (8); allowSpaces? }. char is one symbol or punctuation character. Entries win over people / tags for the same char, field by field, so { char: "@", tone: "info" } keeps the people list. onSearch is debounced 200ms and drops stale answers.

  • allowCreateTagsdefault true
    boolean

    The # list ends with “Create #query” when nothing matches exactly. The created tag (onCreate's result, by default { id: label, label }) is inserted and offered again later.

  • multilinedefault true
    boolean

    A textarea that grows with the text from minRows to maxRows; false renders a single-line input for task titles, where the text scrolls sideways and the chips follow.

  • minRowsdefault 3
    number

    Height of the empty textarea in rows.

  • maxRowsdefault 8
    number

    The textarea grows up to this many rows, then scrolls inside; the chips scroll with the text.

  • placeholderdefault "Write a note. Type @ to mention someone, # to add a tag"
    string

    Shown while the field is empty; it is the best place to teach the triggers.

  • sizedefault "md"
    "sm" | "md" | "lg"

    The field sizes: text 13 / 14 / 15px (16px on phones, so iOS never zooms) and one row as tall as an input of the same size.

  • variantdefault "soft"
    "soft" | "outline" | "ghost"

    soft is a filled well that works on every surface, outline a bordered box, ghost invisible until hovered (inline notes).

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape, scaled per size like the button's (md is 8 / 10 / 12px). full is a pill on one line and half a row height on a taller box.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The chip tint of every trigger without its own tone or color. Neutral is a quiet gray in both themes.

  • colordefault ""
    string

    Any CSS color for the chips; replaces the tone. The tint is mixed at 14% so the text on it stays readable.

  • labeldefault ""
    ReactNode

    Renders the field anatomy with a label linked to the control. Leave it empty for a bare box.

  • descriptiondefault ""
    ReactNode

    Helper text under the box, linked with aria-describedby. The error takes its place while it shows.

  • errordefault ""
    ReactNode

    Replaces the description, slides in and marks the box invalid.

  • requireddefault false
    boolean

    The required mark after the label and required on the control.

  • maxLengthdefault 0
    number

    A limit on the plain text (a chip counts as its visible text). A counter sits in the box, turns amber at 90% and red at the limit, and shakes when a mention would not fit (the insert is refused).

  • suggestionsPlacementdefault "auto"
    "auto" | "caret" | "below" | "above"

    auto opens the list at the caret with a mouse and attaches it to the box edge on touch, above the box when the keyboard would cover the room below. With a mouse the list is portaled above everything, so no card or dialog body ever cuts it. On touch it renders in flow against the box, so it rides every scroll with the field while the keyboard is up; when the parents that clip it (an overflow-hidden card, a sheet or dialog body) leave it too little room, it is portaled instead. A parent that isolates without clipping needs z-dropdown so the in-flow list paints over what follows it.

  • onSubmit
    (value: string, context: MentionChangeContext) => (void | Promise<unknown>)

    Runs on submitShortcut with the markup and { text, mentions }. A promise locks the field (read-only, dimmed, aria-busy) until it settles. Without it the shortcut submits the surrounding form.

  • submitShortcutdefault "mod+enter"
    string

    A combo in mod+k notation, bound on the field only while it has focus. "enter" makes it chat style (a bare Enter still makes new lines on touch keyboards); "" turns it off.

  • disableddefault false
    boolean

    Dims the field and blocks typing.

  • readOnlydefault false
    boolean

    The text and its chips stay visible and selectable; no suggestions open.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • autoFocusdefault false
    boolean

    Focuses the field on mount with the caret at the end of any starting text.

  • namedefault ""
    string

    Adds a hidden input with the markup, so a native form posts the mentions, not just the text.

  • labels
    Partial<MentionInputLabels>

    suggestions “Suggestions”, people “People”, tags “Tags”, create (label, trigger) => “Create “#label””, empty “No matches”, loading “Searching…”, mentioned (label) => “label mentioned”, removed (label) => “label removed”, counter (count, max) => “count of max”, limitReached “Character limit reached”. Exported as DEFAULT_MENTION_INPUT_LABELS.

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "box" | "control" | "highlighter" | "chip" | "suggestions" | "option" | "counter", string>>

    Extra classes for the parts: field, label, description and error style the field anatomy, the rest the box and the list. The highlighter and the control must keep the same font, padding and line height, so style both or neither.

  • className
    string

    Extra classes for the field root (the box when there is no label, description or error).

  • ref
    Ref<HTMLTextAreaElement | HTMLInputElement> (a useRef object or an (el) => void callback)

    The native control. Every other prop (id, aria-*, onKeyDown, onFocus, enterKeyHint) is spread onto it too.

Parts

  • MentionText

    component

    Renders a stored value read-only with the same chips, for comments, audit rows and approval history. Mentions of highlightIds (the current user) take highlightTone; renderMention can wrap a chip in a link. Props: { value: string; triggers?; tone?; color?; highlightIds?: string[]; highlightTone? ("brand"); renderMention?(mention, chip) }.

  • parseMentionMarkup

    function

    Splits markup into { type: "text"; text } and { type: "mention"; trigger; id; label } segments. Unknown ids still parse; empty labels are dropped. Signature: (value: string) => MentionSegment[].

  • getMentions

    function

    Every mention with { trigger, id, label, start, end } in plain-text offsets, e.g. to notify the people mentioned. Signature: (value: string) => MentionRef[].

  • mentionsToText

    function

    The plain text people read: “Call @Daniel Reed tomorrow”. For previews, search indexes and emails. Signature: (value: string) => string.

  • createMentionMarkup

    function

    The markup of one mention, escaped, to build a value on the server or prefill a reply. Signature: (trigger: string, item: { id: string; label: string }) => string.

  • serializeMentionSegments

    function

    The inverse of parseMentionMarkup: segments back to markup, with text that looks like markup escaped. Signature: (segments: MentionSegment[]) => string.

  • DEFAULT_MENTION_INPUT_LABELS

    constant

    The English strings of the suggestion list, the create row, the counter and the screen reader announcements. Spread it and override what you translate.

Examples

Single line

multiline={false} for a task title; Enter adds the task to the list.

Async search

onSearch over 40 colleagues with a 400ms request: a spinner while it loads, “No results” when nothing matches.

Custom trigger

$ links invoices, quotes and contracts, with its own icons and the brand tone.

Tags with create

Tags only, in the info tone; an unknown tag offers “Create “#…”” and is inserted.

Limit and error

maxLength={280} with the counter in the box, and an error until someone is mentioned.

MentionText

Three stored notes rendered read-only; mentions of you (Emma Carter) stand out in the brand tone.

Dependencies

motionfieldlistboxavatarspinnertext-matchuse-controllable-stateuse-media-queryuse-keyboard-insetuse-shortcutmotiontonesui-config

Files

  • components/ui/mention-input.tsx

FAQ

Is the React Mention Input component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Mention Input?

Run npx wingo-ui@latest add mention-input. It also adds the Wingo UI items it builds on (Field, Listbox, Avatar, Spinner, Text match and 7 more) and the npm package motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/mention-input.

How does Mention Input behave on phones?

On a phone the text is 16px, rows are at least 44px and the list attaches to the edge of the box, flipping above it when the keyboard would cover it, so the keyboard never closes.

See also

TextareaComment ThreadTag InputComboboxListbox

Used in

Approval Flow

More React input components

Month PickerMulti SelectNumber InputPassword InputPhone InputRadio Group
All 40 React input components

Articles using this component

  • React Mentions: @People and #Tags That Survive Renames

    Component guides · Oct 19, 2026 · 9 min read

PreviousMasked InputNext Month Picker

New note

Acme Corp · Account #10482 · Austin, TX

AC
Call @Daniel Reed tomorrow about the quote #follow-up
CtrlEnterControl Enterto save

Notes (1)

  • Daniel Reedyesterday, 2:20 PM
    @Emma Carter the client wants a quote for 25 seats by Friday. #quote

Call @[Daniel Reed](u-daniel) tomorrow about the quote #[follow-up](t-follow-up)

mentions: ["u-daniel", "t-follow-up"]

submitShortcut “mod+enter” · placement auto · creates tags · no autoFocus · no hidden input