Kosmesis
Components

Message Scroller

A scrollable chat viewport with an imperative scrollToBottom(). Pairs with Message.

Installation

npx kosmesis add message-scroller
pnpm dlx kosmesis add message-scroller
yarn dlx kosmesis add message-scroller
bunx kosmesis add message-scroller

Copy and paste the following code into your project.

message-scroller.tsx
import { StatefulComponent } from "@praxisjs/core";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface MessageScrollerProps {  class?: string;  children?: Children;}/** * Pairs with `ScrollArea` conceptually, but owns its own scrollable viewport directly rather than * composing it — no Morphos equivalent. Chat UIs need to scroll to the newest message whenever * one is appended; since PraxisJS has no DOM-mutation-observer-style "children changed" hook * built in, call `.scrollToBottom()` explicitly after you push a new message into whatever state * your message list renders from: * * ```tsx * @Ref() scroller!: Ref<MessageScroller> * addMessage(msg: Message) { *   this.messages.push(msg) *   queueMicrotask(() => this.scroller.current?.scrollToBottom()) * } * ``` */@Component()export class MessageScroller extends StatefulComponent {  @Prop() class?: string;  @Prop() children?: MessageScrollerProps["children"];  @Ref<HTMLDivElement>()  viewportRef!: RefType<HTMLDivElement>;  onMount() {    this.scrollToBottom();  }  scrollToBottom(behavior: ScrollBehavior = "auto"): void {    const el = this.viewportRef.current;    if (!el) return;    el.scrollTo({ top: el.scrollHeight, behavior });  }  render() {    return (      <div        ref={this.viewportRef}        data-slot="message-scroller"        class={cn("flex h-full flex-col overflow-y-auto overscroll-contain [scrollbar-width:thin]", this.class)}      >        {this.children}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

message-scroller.tsx
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled } from "@praxisjs/css";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";class MessageScrollerStyles extends Stylesheet {  $root = this.css({    display: "flex",    height: "100%",    flexDirection: "column",    overflowY: "auto",    overscrollBehavior: "contain",    scrollbarWidth: "thin",  });}export interface MessageScrollerProps {  class?: string;  children?: Children;}/** * Pairs with `ScrollArea` conceptually, but owns its own scrollable viewport directly rather than * composing it — no Morphos equivalent. Chat UIs need to scroll to the newest message whenever * one is appended; since PraxisJS has no DOM-mutation-observer-style "children changed" hook * built in, call `.scrollToBottom()` explicitly after you push a new message into whatever state * your message list renders from: * * ```tsx * @Ref() scroller!: Ref<MessageScroller> * addMessage(msg: Message) { *   this.messages.push(msg) *   queueMicrotask(() => this.scroller.current?.scrollToBottom()) * } * ``` */@Component()export class MessageScroller extends StatefulComponent {  @Prop() class?: string;  @Prop() children?: MessageScrollerProps["children"];  @Styled(MessageScrollerStyles) $s!: MessageScrollerStyles;  @Ref<HTMLDivElement>()  viewportRef!: RefType<HTMLDivElement>;  onMount() {    this.scrollToBottom();  }  scrollToBottom(behavior: ScrollBehavior = "auto"): void {    const el = this.viewportRef.current;    if (!el) return;    el.scrollTo({ top: el.scrollHeight, behavior });  }  render() {    return (      <div ref={this.viewportRef} data-slot="message-scroller" class={cx(this.$s.$root, this.class)}>        {this.children}      </div>    );  }}

Examples

Usage

import { Ref } from "@praxisjs/decorators";
import type { Ref as RefType } from "@praxisjs/decorators";
import { Message, MessageGroup } from "@/components/ui/message";
import { MessageScroller } from "@/components/ui/message-scroller";

@Component()
class Chat extends StatefulComponent {
  @State() messages: { from: "user" | "assistant"; text: string }[] = [];

  @Ref<MessageScroller>()
  scroller!: RefType<MessageScroller>;

  addMessage(from: "user" | "assistant", text: string) {
    this.messages = [...this.messages, { from, text }];
    queueMicrotask(() => { this.scroller.current?.scrollToBottom("smooth"); });
  }

  render() {
    return (
      <MessageScroller ref={this.scroller} class="h-96">
        <MessageGroup>
          {() => this.messages.map((m) => <Message from={m.from}>{m.text}</Message>)}
        </MessageGroup>
      </MessageScroller>
    );
  }
}

PraxisJS has no DOM-mutation-observer-style "children changed" hook built in, so scrollToBottom() is called explicitly after appending a message rather than automatically.

On this page