Headless Usage
Full control over rendering. You own the DOM, the library owns the logic.
When to Use
- You want to customize the dropdown layout and positioning
- You need to integrate mentions into an existing UI
- You need complete control over item rendering
For most headless use cases, the compound component API gives you layout control while the library handles the editor internals. Use the raw hook/composable only when you need to control the contenteditable element itself.
Compound Components (Recommended)
import { Mentions } from "@skyastrall/mentions-react";
const users = [
{ id: "1", label: "Alice" },
{ id: "2", label: "Bob" },
];
function Editor() {
return (
<Mentions triggers={[{ char: "@", data: users }]}>
<Mentions.Editor placeholder="Type @..." />
<Mentions.Portal>
<Mentions.List>
<Mentions.Item render={({ item, highlighted }) => (
<div style={{ fontWeight: highlighted ? 600 : 400 }}>
{item.label}
</div>
)} />
</Mentions.List>
</Mentions.Portal>
</Mentions>
);
} <script setup>
import {
Mentions, MentionsEditor, MentionsPortal,
MentionsList, MentionsItem,
} from "@skyastrall/mentions-vue";
const users = [
{ id: "1", label: "Alice" },
{ id: "2", label: "Bob" },
];
</script>
<template>
<Mentions :triggers="[{ char: '@', data: users }]">
<MentionsEditor placeholder="Type @..." />
<MentionsPortal>
<MentionsList>
<MentionsItem v-slot="{ item, highlighted }">
<div :style="{ fontWeight: highlighted ? 600 : 400 }">
{{ item.label }}
</div>
</MentionsItem>
</MentionsList>
</MentionsPortal>
</Mentions>
</template> <script>
import {
Mentions, MentionsEditor, MentionsPortal,
MentionsList, MentionsItem,
} from "@skyastrall/mentions-svelte";
const users = [
{ id: "1", label: "Alice" },
{ id: "2", label: "Bob" },
];
</script>
<Mentions triggers={[{ char: "@", data: users }]}>
<MentionsEditor placeholder="Type @..." />
<MentionsPortal>
<MentionsList>
<MentionsItem>
{#snippet children({ item, highlighted })}
<div style:font-weight={highlighted ? 600 : 400}>
{item.label}
</div>
{/snippet}
</MentionsItem>
</MentionsList>
</MentionsPortal>
</Mentions> Raw Hook / Composable (Full Control)
When you need to render the contenteditable div yourself:
import { useMentions } from "@skyastrall/mentions-react";
function CustomEditor() {
const {
editorRef, inputProps, listProps, getItemProps,
isOpen, items, highlightedIndex, caretPosition,
handleInput,
} = useMentions({
triggers: [{ char: "@", data: users }],
});
return (
<div data-mentions="" style={{ position: "relative" }}>
<div
ref={editorRef}
contentEditable="plaintext-only"
onInput={handleInput}
data-mentions-editor=""
{...inputProps}
/>
{isOpen && (
<ul {...listProps} style={{
position: "fixed",
top: (caretPosition?.top ?? 0) + (caretPosition?.height ?? 0) + 4,
left: caretPosition?.left ?? 0,
}}>
{items.map((item, i) => (
<li key={item.id} {...getItemProps(i)}>
{item.label}
</li>
))}
</ul>
)}
</div>
);
} <script setup>
import { useMentions } from "@skyastrall/mentions-vue";
const users = [
{ id: "1", label: "Alice" },
{ id: "2", label: "Bob" },
];
const {
editorRef, state, aria, isOpen, items,
highlightedIndex, caretPosition,
handleInput, handleKeyDown, handleBlur,
handleCompositionStart, handleCompositionEnd,
performInsertion,
} = useMentions({
triggers: [{ char: "@", data: users }],
});
</script>
<template>
<div data-mentions="" style="position: relative">
<div
ref="editorRef"
contenteditable="plaintext-only"
data-mentions-editor=""
v-bind="aria.inputProps"
@input="handleInput"
@keydown="handleKeyDown"
@blur="handleBlur"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
/>
<ul v-if="isOpen" v-bind="aria.listProps" :style="{
position: 'fixed',
top: (caretPosition?.top ?? 0) + (caretPosition?.height ?? 0) + 4 + 'px',
left: (caretPosition?.left ?? 0) + 'px',
}">
<li
v-for="(item, i) in items"
:key="item.id"
v-bind="aria.getItemProps(i)"
@click="performInsertion(item)"
>
{{ item.label }}
</li>
</ul>
</div>
</template> <script>
import { useMentions } from "@skyastrall/mentions-svelte";
const users = [
{ id: "1", label: "Alice" },
{ id: "2", label: "Bob" },
];
const api = useMentions({
triggers: [{ char: "@", data: users }],
});
</script>
<div data-mentions="" style="position: relative">
<div
bind:this={api.editorRef}
contenteditable="plaintext-only"
data-mentions-editor=""
role={api.aria.inputProps.role}
aria-expanded={api.aria.inputProps["aria-expanded"]}
aria-autocomplete={api.aria.inputProps["aria-autocomplete"]}
aria-haspopup={api.aria.inputProps["aria-haspopup"]}
oninput={() => api.handleInput()}
onkeydown={api.handleKeyDown}
onblur={api.handleBlur}
oncompositionstart={api.handleCompositionStart}
oncompositionend={api.handleCompositionEnd}
></div>
{#if api.isOpen}
<ul
id={api.aria.listProps.id}
role={api.aria.listProps.role}
aria-label={api.aria.listProps["aria-label"]}
style:position="fixed"
style:top="{(api.caretPosition?.top ?? 0) + (api.caretPosition?.height ?? 0) + 4}px"
style:left="{(api.caretPosition?.left ?? 0)}px"
>
{#each api.items as item, i (item.id)}
<li
id={api.aria.getItemProps(i).id}
role="option"
aria-selected={api.aria.getItemProps(i)["aria-selected"]}
onclick={() => api.performInsertion(item)}
>
{item.label}
</li>
{/each}
</ul>
{/if}
</div> Important Attributes
data-mentions=""on a wrapper div — needed for blur detectiondata-mentions-editor=""on the editor — needed for placeholder CSSinputProps/aria.inputPropsincludesrole="combobox"and all ARIA attributes — always spread it
Reading Values
const { markup, plainText, mentions } = useMentions({ triggers });
// markup: "@[Alice](1) hello"
// plainText: "@Alice hello"
// mentions: [{ id: "1", label: "Alice" }] const { markup, plainText, mentions } = useMentions({ triggers });
// markup.value: "@[Alice](1) hello"
// plainText.value: "@Alice hello"
// mentions.value: [{ id: "1", label: "Alice" }] const api = useMentions({ triggers });
// api.markup: "@[Alice](1) hello"
// api.plainText: "@Alice hello"
// api.mentions: [{ id: "1", label: "Alice" }] Programmatic Actions
const { focus, clear, insertTrigger } = useMentions({ triggers });
focus(); // Focus the editor
clear(); // Clear all content
insertTrigger("@"); // Insert @ and open dropdown const { focus, clear, insertTrigger } = useMentions({ triggers });
focus(); // Focus the editor
clear(); // Clear all content
insertTrigger("@"); // Insert @ and open dropdown const api = useMentions({ triggers });
api.focus(); // Focus the editor
api.clear(); // Clear all content
api.insertTrigger("@"); // Insert @ and open dropdown