Mention
The mention component is extends a standard textarea with mention and trigger support (e.g., @username, #tag). It helps users type rich content with interactive mentions by providing:
- Triggers – Define one or more characters (e.g., @, #) that activate mention suggestions.
- Dynamic Options – Pass a list of mentionable items (users, tags, etc.) from the parent component.
- Autocomplete Dropdown – Displays filtered suggestions as the user types after a trigger.
- Keyboard Navigation – Navigate suggestions with arrow keys, confirm with Enter, or cancel with Escape.
- Mentions Extraction – Emits a structured list of selected mentions so the parent can track which entities were referenced.
Examples
The height of the Mention Autosize component automatically adjusts as a response to keyboard inputs and window resizing events.
Code
vue
<script setup lang="ts">
import { computed, ref } from "vue"
type Trigger = '@' | '#'
interface MentionOption {
id: string | number
label: string
link?: string
}
interface Token {
text: string
type: "text" | "mention"
link?: string
}
const text = ref("")
const mentions = ref<MentionOption[]>([])
const userOptions = ref<MentionOption[]>([
{ id: 1, label: "alice" },
{ id: 2, label: "bob" },
{ id: 3, label: "charlie" },
{ id: 4, label: "dwaynejohnson" },
])
const tagOptions = ref<MentionOption[]>([
{ id: "t1", label: "vue" },
{ id: "t2", label: "typescript" }
])
const loading = ref(false)
function onSearch(payload: { trigger: Trigger; query: string }) {
console.log("Searching", payload)
}
// Quick lookup maps (trigger -> { label: link })
const lookup = computed<Record<Trigger, Record<string, string | undefined>>>(() => ({
"@": Object.fromEntries(userOptions.value.map(u => [u.label.toLowerCase(), u.link])),
"#": Object.fromEntries(tagOptions.value.map(t => [t.label.toLowerCase(), t.link])),
}))
// Tokenize preview
const tokens = computed<Token[]>(() => {
const regex = /([@#]\w+)/g
const parts: Token[] = []
let last = 0
let m: RegExpExecArray | null
while ((m = regex.exec(text.value)) !== null) {
if (m.index > last) {
parts.push({ text: text.value.slice(last, m.index), type: "text" })
}
const trig = m[0][0] as Trigger
const lbl = m[0].slice(1).toLowerCase()
parts.push({
text: m[0],
type: "mention",
link: lookup.value[trig]?.[lbl],
})
last = regex.lastIndex
}
if (last < text.value.length) {
parts.push({ text: text.value.slice(last), type: "text" })
}
return parts
})
</script>
<template>
<form @submit.prevent="">
<base-mention v-model="text" :options="{ '@': userOptions, '#': tagOptions }" :triggers="['@', '#']"
:loading="loading" @update:mentions="mentions = $event" @search="onSearch" :min-height="100" border="full" />
</form>
<div style="white-space: pre-wrap;">
<h1>Preview</h1>
<div style="border: #64748b 0.5px solid; padding: 8px; min-height: 100px;">
<template v-for="(t, i) in tokens" :key="i">
<a v-if="t.type === 'mention' && t.link" :href="t.link" target="_blank" style="color: blue;">
{{ t.text }}
</a>
<span v-else-if="t.type === 'mention'" style="color: blue;">
{{ t.text }}
</span>
<span v-else>
{{ t.text }}
</span>
</template>
</div>
</div>
</template>
Mention API
Types
ts
export type BaseMentionBorderType = 'simple' | 'full' | 'none'
export type BaseFormLayoutType = 'vertical' | 'horizontal'
export type MentionOptionsMap = Record<string, IMentionOption[]>
export type SearchType = { payload: { trigger: string; query: string } }
export interface IMentionOption {
id: number | string
label: string
link?: string
}
Props
Name | Type | Default | Description |
---|---|---|---|
v-model | string | v-model is required . | |
v-model:errors | string[] | Mention error message. | |
@update:mentions | IMentionOption[] | Emits the list of mentions currently found in the textarea. | |
@search | SearchType | Fired when a trigger is typed, useful for async option loading. | |
options | MentionOptionsMap | Available mention options grouped by trigger character. | |
triggers | string[] | List of trigger characters to activate mention suggestions. | |
loading | boolean | false | Shows a loading state in the suggestions dropdown (e.g., "Searching…"). |
id | string | Mention id. | |
label | string | Mention label. | |
description | string | Mention description. | |
placeholder | string | Mention placeholder. | |
border | BaseMentionBorderType | Mention border. | |
layout | BaseFormLayoutType | Mention layout. | |
maxlength | number | Mention Max Length. | |
autofocus | boolean | false | Focus mention on page load. |
required | boolean | false | if true mention is required . |
disabled | boolean | false | if true mention is disabled . |
readonly | boolean | false | if true mention is readonly . |
helpers | string[] | Mention helper message. | |
minHeight | number | Mention min height. | |
maxHeight | number | Mention max height. | |
data-testid | string | Testing ID. |
Automated Test Guide
If you pass a data-testid
to the <base-mention>
component, it will automatically generate unique data-testid
attributes for testing.
Gherkin Scenario
txt
When I type "I need to upload a document file here." into "notes"
Step Definition
ts
When('I type {string} into {string}', (value: string, selector: string) => {
cy.get(`[data-testid="${selector}"]`).type(value)
})
Code Implementation
vue
<script setup>
import { ref } from 'vue'
const notes = ref()
</script>
<template>
<base-mention v-model="notes" data-testid="notes" />
</template>