Skip to content

Chat window

A whole channel view: header, scrolling message list and composer, composed from the registry.

A whole channel view: header, scrolling message list and composer, composed from the registry. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import { ChatWindow } from "@/registry/bases/newt/blocks/chat-window/chat-window"

/* Placeholder names and text: no real account, and no Discord asset. */
const MESSAGES = [
  {
    id: "1",
    author: "username",
    initials: "U",
    body: "Welcome to the channel.",
    sentAt: "2026-09-07T14:23:00.000Z",
    displayTime: "Today at 14:23",
  },
  {
    id: "2",
    author: "someone",
    initials: "S",
    body: "The deploy finished — logs look clean.",
    sentAt: "2026-09-07T14:25:00.000Z",
    displayTime: "Today at 14:25",
  },
] as const

export default function ChatWindowDemo() {
  return (
    <ChatWindow
      className="w-full max-w-lg"
      channelName="general"
      topic="Say hello and tell us what you are building."
      messages={MESSAGES}
    />
  )
}
<script setup lang="ts">
import { ChatWindow } from "~~/registry/bases/newt/blocks/chat-window"

/* Placeholder names and text: no real account, and no Discord asset. */
const MESSAGES = [
  {
    id: "1",
    author: "username",
    initials: "U",
    body: "Welcome to the channel.",
    sentAt: "2026-09-07T14:23:00.000Z",
    displayTime: "Today at 14:23",
  },
  {
    id: "2",
    author: "someone",
    initials: "S",
    body: "The deploy finished — logs look clean.",
    sentAt: "2026-09-07T14:25:00.000Z",
    displayTime: "Today at 14:25",
  },
]
</script>

<template>
  <ChatWindow
    class="w-full max-w-lg"
    channel-name="general"
    topic="Say hello and tell us what you are building."
    :messages="MESSAGES"
  />
</template>

Installation

pnpm dlx newtui@latest add chat-window
npx newtui@latest add chat-window
yarn dlx newtui@latest add chat-window
bunx newtui@latest add chat-window

Copy the source into your project

"use client"

import * as React from "react"

import { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
  ChannelHeader,
  ChannelHeaderDivider,
  ChannelHeaderIcon,
  ChannelHeaderInfo,
  ChannelHeaderName,
  ChannelHeaderTopic,
} from "@/registry/bases/newt/ui/channel-header"
import { Chat, ChatBody, ChatFooter } from "@/registry/bases/newt/ui/chat"
import { MessageComposer } from "@/registry/bases/newt/ui/message-composer"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupContent,
  MessageGroupHeader,
  MessageGroupTime,
} from "@/registry/bases/newt/ui/message-group"
import { MessageList } from "@/registry/bases/newt/ui/message-list"

/*
 * A whole channel view: header, scrolling message list, composer. Everything
 * here is composed from registry components — the block is the arrangement,
 * not new markup, so restyling it is still a matter of overriding a token.
 *
 * `messages` is deliberately a plain array. A block does not know your data
 * layer; map onto this shape at the edge of it.
 */
export interface ChatMessage {
  id: string
  author: string
  /** The initials the avatar shows. */
  initials: string
  body: string
  sentAt: string
  /** Rendered timestamp; the ISO value goes in `sentAt`. */
  displayTime: string
}

export interface ChatWindowProps extends React.ComponentProps<"div"> {
  channelName: string
  topic?: string
  messages: readonly ChatMessage[]
  onSend?: (message: string) => void
}

export function ChatWindow({
  channelName,
  topic,
  messages,
  onSend,
  className,
  ...props
}: ChatWindowProps) {
  const [value, setValue] = React.useState("")

  return (
    <div className={className} {...props}>
      <ChannelHeader>
        <ChannelHeaderInfo>
          <ChannelHeaderIcon />
          <ChannelHeaderName>{channelName}</ChannelHeaderName>
          {topic && (
            <>
              <ChannelHeaderDivider />
              <ChannelHeaderTopic>{topic}</ChannelHeaderTopic>
            </>
          )}
        </ChannelHeaderInfo>
      </ChannelHeader>

      <Chat aria-label={`#${channelName} chat`} className="h-96">
        <ChatBody focusable viewportLabel={`#${channelName} messages`}>
          <MessageList label={`Messages in #${channelName}`}>
            {messages.map((message) => (
              <MessageGroup key={message.id}>
                <MessageGroupAvatar>
                  <Avatar>{message.initials}</Avatar>
                </MessageGroupAvatar>
                <MessageGroupBody>
                  <MessageGroupHeader>
                    <MessageGroupAuthor>{message.author}</MessageGroupAuthor>
                    <MessageGroupTime dateTime={message.sentAt}>
                      {message.displayTime}
                    </MessageGroupTime>
                  </MessageGroupHeader>
                  <MessageGroupContent>{message.body}</MessageGroupContent>
                </MessageGroupBody>
              </MessageGroup>
            ))}
          </MessageList>
        </ChatBody>
        <ChatFooter>
          <MessageComposer
            channelName={channelName}
            value={value}
            onValueChange={setValue}
            onSubmit={(message) => {
              onSend?.(message)
              setValue("")
            }}
          />
        </ChatFooter>
      </Chat>
    </div>
  )
}
<script setup lang="ts">
import { ref } from "vue"
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
  ChannelHeader,
  ChannelHeaderDivider,
  ChannelHeaderIcon,
  ChannelHeaderInfo,
  ChannelHeaderName,
  ChannelHeaderTopic,
} from "~~/registry/bases/newt/ui/channel-header"
import { Chat, ChatBody, ChatFooter } from "~~/registry/bases/newt/ui/chat"
import { MessageComposer } from "~~/registry/bases/newt/ui/message-composer"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupContent,
  MessageGroupHeader,
  MessageGroupTime,
} from "~~/registry/bases/newt/ui/message-group"
import { MessageList } from "~~/registry/bases/newt/ui/message-list"

import type { ChatMessage } from "./types"

/*
 * A whole channel view: header, scrolling message list, composer. Everything
 * here is composed from registry components — the block is the arrangement,
 * not new markup, so restyling it is still a matter of overriding a token.
 */
const props = defineProps<{
  channelName: string
  topic?: string
  messages: readonly ChatMessage[]
}>()

const emit = defineEmits<{ send: [message: string] }>()

const value = ref("")

function submit(message: string) {
  emit("send", message)
  value.value = ""
}
</script>

<template>
  <div>
    <ChannelHeader>
      <ChannelHeaderInfo>
        <ChannelHeaderIcon />
        <ChannelHeaderName>{{ props.channelName }}</ChannelHeaderName>
        <template v-if="props.topic">
          <ChannelHeaderDivider />
          <ChannelHeaderTopic>{{ props.topic }}</ChannelHeaderTopic>
        </template>
      </ChannelHeaderInfo>
    </ChannelHeader>

    <Chat :aria-label="`#${props.channelName} chat`" class="h-96">
      <ChatBody focusable :viewport-label="`#${props.channelName} messages`">
        <MessageList :label="`Messages in #${props.channelName}`">
          <MessageGroup v-for="message in props.messages" :key="message.id">
            <MessageGroupAvatar>
              <Avatar>{{ message.initials }}</Avatar>
            </MessageGroupAvatar>
            <MessageGroupBody>
              <MessageGroupHeader>
                <MessageGroupAuthor>{{ message.author }}</MessageGroupAuthor>
                <MessageGroupTime :date-time="message.sentAt">
                  {{ message.displayTime }}
                </MessageGroupTime>
              </MessageGroupHeader>
              <MessageGroupContent>{{ message.body }}</MessageGroupContent>
            </MessageGroupBody>
          </MessageGroup>
        </MessageList>
      </ChatBody>
      <ChatFooter>
        <MessageComposer
          :channel-name="props.channelName"
          v-model:value="value"
          @submit="submit"
        />
      </ChatFooter>
    </Chat>
  </div>
</template>
/*
 * `messages` is deliberately a plain array. A block does not know your data
 * layer; map onto this shape at the edge of it.
 */
export interface ChatMessage {
  id: string
  author: string
  /** The initials the avatar shows. */
  initials: string
  body: string
  /** ISO timestamp, for the `<time>` element. */
  sentAt: string
  /** What the reader sees. */
  displayTime: string
}
export { default as ChatWindow } from "./ChatWindow.vue"
export type { ChatMessage } from "./types"

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { ChatWindow } from "@/components/chat-window"

export function Example() {
  return <ChatWindow />
}
<script setup lang="ts">
import { ChatWindow } from "@/components/chat-window"
</script>

<template>
  <ChatWindow />
</template>

API reference

ChatWindow

PropType
channelName?string

Required channelName value.

Typestring
topic?string | undefined

Optional topic value.

Typestring | undefined
messages?readonly ChatMessage[]

Required messages value.

Typereadonly ChatMessage[]
onSend?(message: string) => void | undefined

Optional onSend value.

Type(message: string) => void | undefined

The registry exports ChatMessage, ChatWindow. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.

Tokens

Chat window reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer.