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-listnpx newtui@latest add message-listyarn dlx newtui@latest add message-listbunx newtui@latest add message-listAdd 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
label?string | undefined
Accessible name, e.g. `Messages in #general`.
string | undefinedThe 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.