Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new
DocumentationMessageScroller

MessageScroller

A chat container with initial positioning, streaming and history loading. Set a bounded height. The default position is end; start opens at the first message, and last-anchor opens at the final user turn when that turn is taller than the viewport. End and last-anchor stay hidden until initial positioning completes, including server rendering and hydration. An empty transcript becomes visible after mounting. Auto-scroll follows growing replies until the reader scrolls away. Prepending history keeps the visible message in place.

Interactive preview

Rendered from the shared Frontfriend stories with React Base, the same preview Studio shows. Styling comes from your Design System; the code below is native to Reka UI.

Installation

Complete project setup first. Your configuration must select vue/reka and include your Design System ID.

pnpm exec frontfriend download message-scroller -f vue --primitive reka

Review dependencies reported by the downloader and keep your source changes in version control.

Usage

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "autoScroll": true,
  "defaultScrollPosition": "end",
  "height": 300,
  "streaming": undefined,
  "history": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown> & {
    defaultScrollPosition?: "start" | "end" | "last-anchor";
  };
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

The public useMessageScroller, useMessageScrollerScrollable and useMessageScrollerVisibility hooks provide scroll commands, scroll-edge state and visible message IDs. Call them within the provider. Vue exposes the same three composables, with reactive refs for state and visibility.

Import useMessageScroller, useMessageScrollerScrollable and useMessageScrollerVisibility from the component barrel and call them in setup within the provider. The first composable returns scrollToStart, scrollToEnd and scrollToMessage. The other two return reactive refs with scroll-edge state and visible message IDs.

Examples

Default

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "streaming": undefined,
  "history": undefined,
  "height": undefined,
  "autoScroll": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown>;
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

Initial Start

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "defaultScrollPosition": "start",
  "streaming": undefined,
  "history": undefined,
  "height": undefined,
  "autoScroll": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown>;
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

Last Anchor

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "defaultScrollPosition": "last-anchor",
  "streaming": undefined,
  "history": undefined,
  "height": undefined,
  "autoScroll": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown>;
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

Streaming

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "autoScroll": true,
  "streaming": true,
  "history": undefined,
  "height": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown>;
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

Prepended History

<script setup lang="ts">
import { computed } from "vue";
import { ref } from "vue";
import { Button } from "@/components/ui/button";
import { MessageScrollerProvider } from "@/components/ui/message-scroller";
import { MessageScroller } from "@/components/ui/message-scroller";
import { MessageScrollerViewport } from "@/components/ui/message-scroller";
import { MessageScrollerContent } from "@/components/ui/message-scroller";
import { MessageScrollerItem } from "@/components/ui/message-scroller";
import { Message } from "@/components/ui/message";
import { MessageAvatar } from "@/components/ui/message";
import { Avatar } from "@/components/ui/avatar";
import { AvatarFallback } from "@/components/ui/avatar";
import { MessageContent } from "@/components/ui/message";
import { Bubble } from "@/components/ui/bubble";
import { BubbleContent } from "@/components/ui/bubble";
import { MessageScrollerButton } from "@/components/ui/message-scroller";

type PreviewProps = {
  defaultScrollPosition?: "start" | "end" | "last-anchor";
  autoScroll?: boolean;
  height?: number;
  streaming?: boolean;
  history?: boolean;
};

const reply =
  "Use the component source for your selected target. The design system supplies the styles. Keep message IDs stable when you load history or update a reply. ";

const args = {
  "defaultScrollPosition": "start",
  "history": true,
  "streaming": undefined,
  "height": undefined,
  "autoScroll": undefined,
} satisfies Partial<
  InstanceType<typeof MessageScroller> extends { $props: infer Props }
    ? Props
    : {}
> &
  Record<string, unknown>;
const initialPosition = computed(
  () => (args as PreviewProps).defaultScrollPosition ?? "end",
);
const messages = ref(
  Array.from({ length: 12 }, (_, index) => ({
    id: `message-${index}`,
    user: index % 2 === 0,
    text:
      index % 2 === 0
        ? `Question ${index / 2 + 1}: How do I build this conversation?`
        : reply.repeat(index === 11 ? 10 : 2),
  })),
);
const updates = ref(0);
function append() {
  messages.value[messages.value.length - 1]!.text += reply;
  updates.value += 1;
}
function prepend() {
  messages.value.unshift({
    id: `history-${messages.value.length}`,
    user: false,
    text: "Earlier message. " + reply.repeat(2),
  });
  updates.value += 1;
}
</script>

<template>
  <div
    class="flex w-[420px] max-w-[calc(100vw-2rem)] flex-col gap-3 text-neutral-strong"
  >
    <div
      v-if="args.streaming || args.history"
      class="flex flex-wrap items-center gap-2"
    >
      <Button
        type="button"
        variant="outline"
        @click="args.streaming ? append() : prepend()"
        >{{
          args.streaming ? "Append response" : "Load earlier messages"
        }}</Button
      >
      <span role="status" class="text-sm text-neutral-mid"
        >Updates: {{ updates }}</span
      >
    </div>
    <div
      class="w-full overflow-hidden rounded-xl border border-neutral-subtle"
      :style="{ height: (args.height || 420) + 'px' }"
    >
      <MessageScrollerProvider
        :default-scroll-position="initialPosition"
        :auto-scroll="args.autoScroll || false"
      >
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              <MessageScrollerItem
                v-for="message in messages"
                :key="message.id"
                :message-id="message.id"
                :scroll-anchor="message.user"
              >
                <div class="px-4 py-2">
                  <Message :align="message.user ? 'end' : undefined">
                    <MessageAvatar
                      ><Avatar
                        ><AvatarFallback>{{
                          message.user ? "ME" : "AI"
                        }}</AvatarFallback></Avatar
                      ></MessageAvatar
                    >
                    <MessageContent
                      ><Bubble :variant="message.user ? undefined : 'muted'"
                        ><BubbleContent>{{
                          message.text
                        }}</BubbleContent></Bubble
                      ></MessageContent
                    >
                  </Message>
                </div>
              </MessageScrollerItem>
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton direction="end" />
          <MessageScrollerButton direction="start" />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  </div>
</template>

API reference

Frontfriend-owned props. Native HTML and inherited primitive props remain documented by Reka UI.

MessageScroller

Prop Type Default Description
class HTMLAttributes["class"] —

MessageScrollerButton

Prop Type Default Description
behavior ScrollBehavior —
class HTMLAttributes["class"] —
direction MessageScrollerButtonDirection —
size ButtonVariants["size"] —
variant ButtonVariants["variant"] —

MessageScrollerContent

Prop Type Default Description
class HTMLAttributes["class"] —
spacerClass HTMLAttributes["class"] —

MessageScrollerItem

Prop Type Default Description
class HTMLAttributes["class"] —
messageId string —
scrollAnchor boolean —

MessageScrollerViewport

Prop Type Default Description
class HTMLAttributes["class"] —
preserveScrollOnPrepend boolean —

Configuration determines the actual classes and available styling hooks. See Theming & component configuration before introducing a repeated local override.