Skip to content

Headless Mode

Use useAnchor() and usePresence() for full control over the UI while keeping all the discussion logic.

useAnchor(anchorId)

ts
import { useAnchor } from '@anchor-sdk/vue'

const {
  // State
  threads, // Ref<Thread[]>
  loading, // Ref<boolean>
  error, // Ref<Error | null>
  open, // Ref<boolean>
  messageCount, // ComputedRef<number>
  unreadCount, // ComputedRef<number>
  hasThreads, // ComputedRef<boolean>
  currentUser, // User

  // Quick actions
  send, // (content: string, options?: MessageOptions) => Promise<void>
  toggle, // () => void
  show, // () => void
  hide, // () => void
  markAsRead, // () => void

  // Full thread API (all with optimistic updates)
  createThread, // (content, options?) => Promise<void>
  addMessage, // (threadId, content, options?) => Promise<void>
  editMessage, // (messageId, content) => Promise<Message>
  deleteMessage, // (threadId, messageId) => Promise<void>
  resolveThread, // (threadId) => Promise<void>
  reopenThread, // (threadId) => Promise<void>
  deleteThread, // (threadId) => Promise<void>
  addReaction, // (messageId, emoji) => Promise<void>
  removeReaction, // (messageId, emoji) => Promise<void>
  uploadAttachment, // (file: File) => Promise<Attachment | undefined>
  refresh, // () => Promise<void>
} = useAnchor('my-element')

All mutations use optimistic updates — the UI updates instantly and rolls back automatically on error.

usePresence(anchorId)

ts
import { usePresence } from '@anchor-sdk/vue'

const {
  presence, // Ref<PresenceInfo[]> — who's online
  typingUsers, // Ref<User[]> — who's typing
  setOnline, // () => Promise<void>
  setAway, // () => Promise<void>
  setOffline, // () => Promise<void>
  startTyping, // () => Promise<void>
  stopTyping, // () => Promise<void>
} = usePresence('my-element')

The composable auto-sets the user online on mount and offline on unmount. Requires an adapter with presence support (e.g. createMemoryAdapter or createWebSocketAdapter).

useMentions(options)

Headless @mention autocomplete. Drives your own suggestion popup using any text input.

ts
import { useMentions } from '@anchor-sdk/vue'
import { ref } from 'vue'

const text = ref('')
const m = useMentions({
  text,
  resolveUsers: () => teamMembers, // sync or async
  filter: (u) => u.id !== currentUser.id,
})

function onInput(e: Event) {
  const el = e.target as HTMLTextAreaElement
  m.onInput(el.selectionStart ?? el.value.length)
}
</script>

<template>
  <textarea v-model="text" @input="onInput" />
  <ul v-if="m.active.value">
    <li
      v-for="(u, i) in m.suggestions.value"
      :key="u.id"
      :class="{ active: i === m.selectedIndex.value }"
      @mousedown.prevent="
        {
          const r = m.select(textarea.selectionStart ?? 0, u)
          if (r) text = r.text
        }
      "
    >
      @{{ u.name }}
    </li>
  </ul>
</template>

The composable only manages state — you own the rendering, positioning, and input binding.

Attachments

Enable file/image attachments by implementing uploadAttachment on your adapter, then:

ts
const { uploadAttachment, send } = useAnchor('my-element')

async function onFile(file: File) {
  const att = await uploadAttachment(file)
  if (att) await send('see attached', { attachments: [att] })
}

If the adapter doesn't implement attachments, uploadAttachment returns undefined and sets error.

Example: Custom Chat UI

vue
<script setup lang="ts">
import { useAnchor } from '@anchor-sdk/vue'

const { threads, send, open, toggle, messageCount } = useAnchor('custom-chat')
const input = ref('')

function handleSend() {
  if (input.value.trim()) {
    send(input.value)
    input.value = ''
  }
}
</script>

<template>
  <div class="my-chat">
    <button @click="toggle">Comments ({{ messageCount }})</button>

    <div v-if="open" class="my-chat-panel">
      <div v-for="thread in threads" :key="thread.id">
        <div v-for="msg in thread.messages" :key="msg.id">
          <b>{{ msg.user.name }}</b
          >: {{ msg.content }}
        </div>
      </div>

      <input v-model="input" @keydown.enter="handleSend" />
      <button @click="handleSend">Send</button>
    </div>
  </div>
</template>