Components
Message Scroller
A scrollable chat viewport with an imperative scrollToBottom(). Pairs with Message.
Installation
npx kosmesis add message-scrollerpnpm dlx kosmesis add message-scrolleryarn dlx kosmesis add message-scrollerbunx kosmesis add message-scrollerCopy and paste the following code into your project.
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/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
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.