Headless Usage

Full control over rendering. You own the DOM, the library owns the logic.

When to Use

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

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