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 are React / Vue / Svelte only — in Angular, use <sa-mentions> for the batteries-included path, or the bridge below for full control.)

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. Angular has no hook — drive the MentionsControllerBridge directly (it wraps the same core controller that <sa-mentions> uses internally; the DOM-read helpers live in @skyastrall/mentions-core):

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>
import { Component, DestroyRef, inject } from "@angular/core";
import { MentionsControllerBridge } from "@skyastrall/mentions-angular";

@Component({ /* your own template with a contenteditable div */ })
export class CustomEditor {
  triggers = [{ char: "@", data: users }];
  bridge = new MentionsControllerBridge({ triggers: this.triggers }, inject(DestroyRef));

  // bridge.state() is a signal:
  //   .status / .items / .query / .highlightedIndex / .caretPosition
  // Read the DOM, then feed the controller:
  onInput(markup: string, plainText: string, cursor: number) {
    this.bridge.controller.handleInputChange(markup, plainText, cursor);
  }
  onKeyDown(e: KeyboardEvent) {
    if (this.bridge.controller.handleKeyDown(e.key).handled) e.preventDefault();
  }
}

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" }]
// From the bridge's state signal:
bridge.state().markup;     // "@[Alice](1) hello"
bridge.state().plainText;  // "@Alice hello"

// Or from a <sa-mentions> template ref:
ref.getValue();            // { markup, plainText }

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
// Call methods on the <sa-mentions> template ref:
ref.focus();              // Focus the editor
ref.clear();              // Clear all content
ref.insertTrigger("@");   // Insert @ and open dropdown
ref.insertText("👍");     // Insert arbitrary text at the caret