Using primitives

Unstyled Vue components that bind native elements to assistant state, and the conventions they share.

Primitives are unstyled components exported from @assistant-ui/vue. Each one reads state from the nearest scope and renders a native element, plain text, or nothing of its own. Layout, styling, and icons stay in your templates. The styled components are built from the same primitives.

app/components/MinimalThread.vue
<script setup lang="ts">
import {
  ComposerPrimitiveInput,
  ComposerPrimitiveSend,
  MessagePrimitiveParts,
  MessagePrimitiveRoot,
  ThreadPrimitiveMessages,
  ThreadPrimitiveViewport,
  ThreadPrimitiveViewportFooter,
} from "@assistant-ui/vue";
</script>

<template>
  <ThreadPrimitiveViewport class="flex h-dvh flex-col overflow-y-auto">
    <ThreadPrimitiveMessages>
      <MessagePrimitiveRoot class="px-4 py-2">
        <MessagePrimitiveParts />
      </MessagePrimitiveRoot>
    </ThreadPrimitiveMessages>
    <ThreadPrimitiveViewportFooter class="bg-background sticky bottom-0 mt-auto flex gap-2 p-4">
      <ComposerPrimitiveInput class="flex-1 resize-none" aria-label="Message" rows="1" />
      <ComposerPrimitiveSend>Send</ComposerPrimitiveSend>
    </ThreadPrimitiveViewportFooter>
  </ThreadPrimitiveViewport>
</template>

Render it under an AuiProvider with a runtime, as in the quickstart.

Names

Every primitive is a named export with its family in the name. Where React writes ThreadPrimitive.Viewport, Vue imports ThreadPrimitiveViewport, so templates use one PascalCase tag per part.

FamilyExports
ThreadThreadPrimitiveRoot, ThreadPrimitiveViewport, ThreadPrimitiveViewportFooter, ThreadPrimitiveScrollToBottom, ThreadPrimitiveMessages, ThreadPrimitiveSuggestions
MessageMessagePrimitiveRoot, MessagePrimitiveParts, MessagePrimitiveAttachments
ComposerComposerPrimitiveInput, ComposerPrimitiveSend, ComposerPrimitiveCancel, ComposerPrimitiveAttachments, ComposerPrimitiveAddAttachment, ComposerPrimitiveAttachmentDropzone
Action barActionBarPrimitiveEdit, ActionBarPrimitiveReload, ActionBarPrimitiveCopy
Branch pickerBranchPickerPrimitivePrevious, BranchPickerPrimitiveNext, BranchPickerPrimitiveNumber, BranchPickerPrimitiveCount
Thread listThreadListPrimitiveRoot, ThreadListPrimitiveNew, ThreadListPrimitiveItems, ThreadListPrimitiveLoadMore, ThreadListItemPrimitiveRoot, ThreadListItemPrimitiveTrigger, ThreadListItemPrimitiveTitle, ThreadListItemPrimitiveArchive, ThreadListItemPrimitiveUnarchive, ThreadListItemPrimitiveDelete
AttachmentAttachmentPrimitiveRoot, AttachmentPrimitiveName, AttachmentPrimitiveThumb, AttachmentPrimitiveRemove
SuggestionSuggestionPrimitiveTrigger, SuggestionPrimitiveTitle, SuggestionPrimitiveDescription
ErrorErrorPrimitiveRoot, ErrorPrimitiveMessage
Chain of thoughtChainOfThoughtPrimitiveParts, ChainOfThoughtPrimitiveAccordionTrigger

Attributes and listeners

A part that renders an element passes your attributes and listeners to that element: class, style, aria-*, data-*, id, and native listeners such as @click or @keydown. There is no asChild prop. Style the rendered element directly, or wrap it in your own component.

Your listener runs before the primitive's own handler. Call preventDefault() in it to stop the primitive's action for that event:

app/components/GuardedNewThread.vue
<script setup lang="ts">
import { ThreadListPrimitiveNew, useAui } from "@assistant-ui/vue";

const aui = useAui();

const holdWhileRunning = (event: MouseEvent) => {
  if (aui.thread.getState().isRunning) event.preventDefault();
};
</script>

<template>
  <ThreadListPrimitiveNew @click="holdWhileRunning">New chat</ThreadListPrimitiveNew>
</template>

A disabled attribute disables a button part in addition to its own rules. A bare disabled and :disabled="true" disable it; :disabled="false" and the string "false" leave it enabled. While a part is disabled by its own state, the native disabled attribute is set, so disabled: utility variants style it.

Iterators and slots

Parts that repeat content render no element of their own. Each one renders its default slot once per item and scopes that slot to the item, so components inside it read s.message, s.part, s.attachment, s.suggestion, or s.threadListItem without props:

IteratorRepeats forScope inside the slot
ThreadPrimitiveMessagesMessages in the threads.message, and its edit composer as s.composer
MessagePrimitivePartsParts of the messages.part
MessagePrimitiveAttachmentsAttachments on a user messages.attachment
ComposerPrimitiveAttachmentsPending composer attachmentss.attachment
ThreadPrimitiveSuggestionsSuggestionss.suggestion
ThreadListPrimitiveItemsThreads in the lists.threadListItem
ChainOfThoughtPrimitivePartsParts in the chain-of-thought scopes.part, and the slot prop { part }

Where React passes a render function or a components prop, Vue uses slots. MessagePrimitiveParts takes one named slot per part type (#text, #reasoning, #tool-call, and so on), described on the message page.

Rows are keyed by identity, such as the message or thread id, so per-row component state follows the item when the list changes.

To iterate yourself, wrap each row in the matching scope provider: MessageByIdProvider, PartByIndexProvider, AttachmentByIndexProvider, SuggestionByIndexProvider, or ThreadListItemByIndexProvider. The thread list guide shows the pattern.

Text parts

Some parts render text with no element of their own: BranchPickerPrimitiveNumber, BranchPickerPrimitiveCount, SuggestionPrimitiveTitle, SuggestionPrimitiveDescription, AttachmentPrimitiveName, ThreadListItemPrimitiveTitle, and ErrorPrimitiveMessage. Wrap them in an element to style them. Several accept a default slot that replaces the text.

State attributes

AttributeSet onPresent while
data-active="true", aria-current="true"ThreadListPrimitiveNewThe new thread is the selected thread.
data-active="true", aria-current="true"ThreadListItemPrimitiveRoot, ThreadListItemPrimitiveTriggerThe item's thread is selected.
data-copiedActionBarPrimitiveCopyA copy succeeded, for copiedDuration milliseconds.
data-dragging="true"ComposerPrimitiveAttachmentDropzoneA file drag hovers the element.
data-message-idMessagePrimitiveRootAlways; the value is the message id.
role="alert"ErrorPrimitiveRootAlways.

Compared with React

The Vue package has no assistant modal, selection toolbar, action bar root, feedback, speech, or export buttons, composer form root, mention and slash command popovers, dictation, quote, or message queue parts, and no thread list item menu. Build those from your own components, or from a headless library such as reka-ui, and call the scope methods through useAui.