Mention input component for PhiaUI.
A textarea with @mention autocomplete support. When the user types @,
the PhiaMentionInput JS hook detects the trigger character and emits a
push_event("mention_search", %{query: query}) to the server. The LiveView
filters the user list, reassigns suggestions, and re-renders the dropdown.
When a suggestion is selected, the hook inserts a styled chip into the
textarea and the mentioned user IDs are tracked in a hidden CSV input for
form submission.
When to use
Use MentionInput anywhere users should be able to tag other users with
@username:
- Team collaboration comment threads (GitHub/Linear style)
- Task assignment descriptions
- Chat messages in a team messenger
- Document editor annotations
- Support ticket replies
Anatomy
| Component | Element | Purpose |
|---|---|---|
mention_input/1 | div | Root wrapper (textarea + hook + hidden input + dropdown) |
mention_dropdown/1 | ul | Suggestion listbox — hidden when open: false |
mention_chip/1 | span | Inline @name highlight for server-rendered previews |
How it works
- User types
@in the textarea PhiaMentionInputhook firespush_event("mention_search", %{query: ""})- LiveView runs
handle_event("mention_search", ...)→ assigns suggestions - User types more → hook fires
push_event("mention_search", %{query: "ali"}) - LiveView filters and re-assigns suggestions; dropdown opens (
mention_open: true) - User clicks / arrows to a suggestion →
phx-click="mention_select" - LiveView appends the ID to
mentioned_ids; hook inserts a chip into the textarea - On form submit, the hidden input
name_idscarries the CSV of mentioned IDs
Complete example
defmodule MyAppWeb.CommentLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket,
mention_suggestions: [],
mention_open: false,
mention_search: "",
mentioned_ids: []
)}
end
# Hook emits push_event; LiveView handles it as a regular event
def handle_event("mention_search", %{"query" => q}, socket) do
suggestions =
Users.search(q, limit: 8)
|> Enum.map(&%{id: &1.id, name: &1.name, avatar: &1.avatar_url})
{:noreply, assign(socket,
mention_suggestions: suggestions,
mention_open: true,
mention_search: q
)}
end
def handle_event("mention_select", %{"id" => id, "name" => _name}, socket) do
ids = [id | socket.assigns.mentioned_ids] |> Enum.uniq()
{:noreply, assign(socket,
mentioned_ids: ids,
mention_open: false,
mention_search: ""
)}
end
end
<%!-- Template --%>
<.mention_input
id="comment-body"
name="comment[body]"
suggestions={@mention_suggestions}
open={@mention_open}
search={@mention_search}
mentioned_ids={@mentioned_ids}
on_mention="mention_search"
on_select="mention_select"
placeholder="Leave a comment… type @ to mention someone"
/>Displaying resolved mentions in read-only content
When rendering submitted comment content (e.g. in a feed), use mention_chip/1
to highlight resolved user mentions with the same visual style:
<p>
Hey
<.mention_chip name="Alice" user_id={alice.id} />
the PR is ready for review.
</p>Hook setup
# app.js
import PhiaMentionInput from "./hooks/mention_input"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaMentionInput }
})ARIA
The <textarea> carries role="combobox", aria-autocomplete="list",
aria-haspopup="listbox", and aria-expanded toggled by the open assign.
The dropdown has role="listbox". Each suggestion option has role="option".
Summary
Functions
Renders an inline @mention highlight chip for server-rendered content.
Renders the @mention suggestion dropdown.
Renders the mention input widget.
Functions
Renders an inline @mention highlight chip for server-rendered content.
Use this component when displaying already-saved content that contains resolved mentions — for example, rendering a comment body from the database where mentioned user IDs have been resolved to names.
In client-side mode, the PhiaMentionInput hook inserts chips dynamically
into the textarea. mention_chip/1 is for the read-only rendering path.
Example
<p>
<.mention_chip name="Sarah Lin" user_id={sarah.id} />
can you review the attached document?
</p>Attributes
name(:string) (required) - Display name of the mentioned user (rendered as@name).user_id(:string) (required) - User ID stored in thedata-mention-idattribute for client-side processing.class(:string) - Additional CSS classes for the chip span. Defaults tonil.
Renders the @mention suggestion dropdown.
Uses two function heads:
open: false→ renders~H""(empty fragment — no DOM element at all)open: true→ renders the<ul role="listbox">with suggestion items
This pattern is preferred over CSS visibility toggling because it keeps the DOM clean and prevents screen readers from announcing the hidden list.
Each suggestion fires on_select with phx-value-id and phx-value-name.
Attributes
id(:string) - DOM id for the listbox element — referenced byaria-controlson the textarea. Defaults tonil.suggestions(:list) - List of%{id, name, avatar}suggestion maps. Defaults to[].open(:boolean) - Controls dropdown visibility. Uses two function heads: false → empty fragment. Defaults tofalse.on_select(:string) -phx-clickevent name fired when a suggestion is chosen. Defaults to"mention_select".class(:string) - Additional CSS classes for the listbox<ul>. Defaults tonil.
Renders the mention input widget.
The root div contains:
- A
<textarea>wired to thePhiaMentionInputhook - A hidden
<input>that holds the CSV of mentioned user IDs - A
mention_dropdown/1suggestion panel controlled by theopenassign
The LiveView controls open/close state and the suggestion list. The hook
handles @ detection in the textarea and DOM insertion of @name chips.
Attributes
id(:string) (required) - DOM id for the textarea element. ThePhiaMentionInputhook is anchored to this element and uses it to identify the instance.name(:string) (required) - Form field name for the textarea content. The hidden input for mentioned IDs will be named#{name}_ids.suggestions(:list) - List of%{id: string, name: string, avatar: string | nil}suggestion maps. Updated by the LiveView in response toon_mentionevents. An empty list renders "No results" inside the open dropdown.Defaults to
[].open(:boolean) - Whether the suggestion dropdown is currently visible. Set totrueinhandle_event("mention_search", ...)andfalseinhandle_event("mention_select", ...).Defaults to
false.search(:string) - Current@mentionsearch query typed by the user. Defaults to"".value(:string) - Current textarea text value (for server-rendered initial content). Defaults to"".mentioned_ids(:list) - List of already-selected user ID strings. Serialised as a comma-separated value in the hiddenname_idsinput for form submission and changeset processing.Defaults to
[].on_mention(:string) - Event name that the hook emits viapush_eventwhen the user types after@. The LiveView receives%{"query" => query}.Defaults to
nil.on_select(:string) -phx-clickevent name fired when a suggestion is selected. The LiveView receives%{"id" => user_id, "name" => user_name}.Defaults to
"mention_select".placeholder(:string) - Textarea placeholder text. Defaults to"Type a message… use @ to mention".class(:string) - Additional CSS classes for the root wrapper div. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div.