Skip to content

Message List

The polite live region that logs grouped messages in a channel.

The polite live region that logs grouped messages in a channel. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupContent,
  MessageGroupHeader,
  MessageGroupTime,
} from "@/registry/bases/newt/ui/message-group"
import { MessageList } from "@/registry/bases/newt/ui/message-list"

export default function MessageListDemo() {
  return (
    <MessageList label="Messages in #general" className="w-full max-w-md">
      <MessageGroup>
        <MessageGroupAvatar>
          <Avatar>U</Avatar>
        </MessageGroupAvatar>
        <MessageGroupBody>
          <MessageGroupHeader>
            <MessageGroupAuthor>username</MessageGroupAuthor>
            <MessageGroupTime dateTime="2026-09-04T14:23:00.000Z">
              Today at 14:23
            </MessageGroupTime>
          </MessageGroupHeader>
          <MessageGroupContent>First message.</MessageGroupContent>
        </MessageGroupBody>
      </MessageGroup>
      <MessageGroup>
        <MessageGroupAvatar>
          <Avatar>B</Avatar>
        </MessageGroupAvatar>
        <MessageGroupBody>
          <MessageGroupHeader>
            <MessageGroupAuthor>someone</MessageGroupAuthor>
            <MessageGroupTime dateTime="2026-09-04T14:24:00.000Z">
              Today at 14:24
            </MessageGroupTime>
          </MessageGroupHeader>
          <MessageGroupContent>Second message.</MessageGroupContent>
        </MessageGroupBody>
      </MessageGroup>
    </MessageList>
  )
}
<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupContent,
  MessageGroupHeader,
  MessageGroupTime,
} from "~~/registry/bases/newt/ui/message-group"
import { MessageList } from "~~/registry/bases/newt/ui/message-list"
</script>

<template>
  <MessageList label="Messages in #general" class="w-full max-w-md">
    <MessageGroup>
      <MessageGroupAvatar>
        <Avatar>U</Avatar>
      </MessageGroupAvatar>
      <MessageGroupBody>
        <MessageGroupHeader>
          <MessageGroupAuthor>username</MessageGroupAuthor>
          <MessageGroupTime datetime="2026-09-04T14:23:00.000Z">
            Today at 14:23
          </MessageGroupTime>
        </MessageGroupHeader>
        <MessageGroupContent>First message.</MessageGroupContent>
      </MessageGroupBody>
    </MessageGroup>
    <MessageGroup>
      <MessageGroupAvatar>
        <Avatar>B</Avatar>
      </MessageGroupAvatar>
      <MessageGroupBody>
        <MessageGroupHeader>
          <MessageGroupAuthor>someone</MessageGroupAuthor>
          <MessageGroupTime datetime="2026-09-04T14:24:00.000Z">
            Today at 14:24
          </MessageGroupTime>
        </MessageGroupHeader>
        <MessageGroupContent>Second message.</MessageGroupContent>
      </MessageGroupBody>
    </MessageGroup>
  </MessageList>
</template>

Installation

pnpm dlx newtui@latest add message-list
npx newtui@latest add message-list
yarn dlx newtui@latest add message-list
bunx newtui@latest add message-list

Add the components it builds on

Copy the source into your project

import * as React from "react"

import { cn } from "@/lib/utils"

/*
 * The log of grouped messages in a channel. `role="log"` with a polite live
 * region is what makes an arriving message announce itself without stealing
 * focus from whatever the reader is doing.
 */
export interface MessageListProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Accessible name, e.g. `Messages in #general`. */
  label?: string
}

const MessageList = React.forwardRef<HTMLDivElement, MessageListProps>(
  ({ className, label, ...props }, ref) => (
    <div
      ref={ref}
      role="log"
      aria-live="polite"
      aria-relevant="additions text"
      aria-label={label}
      className={cn(
        "flex min-w-0 flex-col px-2 pb-4 pt-1 sm:px-4",
        /* Rows sit flush: the list owns the padding, not the row. */
        "[&>[data-slot=message-row]]:rounded-sm [&>[data-slot=message-row]]:px-2 [&>[data-slot=message-row]]:py-1.5",
        className
      )}
      {...props}
    />
  )
)
MessageList.displayName = "MessageList"

export { MessageList }
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

/*
 * The log of grouped messages in a channel. `role="log"` with a polite live
 * region is what makes an arriving message announce itself without stealing
 * focus from whatever the reader is doing.
 */
const props = defineProps<{
  class?: HTMLAttributes["class"]
  /** Accessible name, e.g. `Messages in #general`. */
  label?: string
}>()
</script>

<template>
  <div
    role="log"
    aria-live="polite"
    aria-relevant="additions text"
    :aria-label="props.label"
    :class="
      cn(
        'flex min-w-0 flex-col px-2 pb-4 pt-1 sm:px-4',
        // Rows sit flush: the list owns the padding, not the row.
        '[&>[data-slot=message-row]]:rounded-sm [&>[data-slot=message-row]]:px-2 [&>[data-slot=message-row]]:py-1.5',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
export { default as MessageList } from "./MessageList.vue"

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 { MessageList } from "@/components/ui/message-list"

export function Example() {
  return <MessageList />
}
<script setup lang="ts">
import { MessageList } from "@/components/ui/message-list"
</script>

<template>
  <MessageList />
</template>

API reference

MessageList

PropType
label?string | undefined

Accessible name, e.g. `Messages in #general`.

Typestring | undefined

The registry exports MessageList. 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

Message List 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.