Skip to content

August 2026 — Components rework

Twelve new components, an opt-in light palette, and a pass over every existing component against the original specs.

The registry grew from 43 components to 55, gained a light surface set, and every component that already existed was reworked against its original spec.

New components

InlineCode, ActionRow, MessageList, MessageComposer, Chat, Invite, ChannelHeader, ChannelWelcome, ChannelInfo, SlashCommandSuggestions, V2Container and AppLauncher, plus a ReactionGroup part on Reaction.

"use client"

import * as React from "react"

import { Avatar } from "@/registry/bases/newt/ui/avatar"
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"

export default function ChatDemo() {
  const [value, setValue] = React.useState("")

  return (
    <Chat aria-label="#general channel chat" className="h-80 w-full max-w-md">
      <ChatBody focusable viewportLabel="#general channel messages">
        <MessageList label="Messages in #general">
          <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>Welcome to the channel.</MessageGroupContent>
            </MessageGroupBody>
          </MessageGroup>
        </MessageList>
      </ChatBody>
      <ChatFooter>
        <MessageComposer
          channelName="general"
          value={value}
          onValueChange={setValue}
          onSubmit={() => setValue("")}
        />
      </ChatFooter>
    </Chat>
  )
}
<script setup lang="ts">
import { ref } from "vue"
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
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"

const value = ref("")
</script>

<template>
  <Chat aria-label="#general channel chat" class="h-80 w-full max-w-md">
    <ChatBody focusable viewport-label="#general channel messages">
      <MessageList label="Messages in #general">
        <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>Welcome to the channel.</MessageGroupContent>
          </MessageGroupBody>
        </MessageGroup>
      </MessageList>
    </ChatBody>
    <ChatFooter>
      <MessageComposer
        v-model="value"
        channel-name="general"
        @submit="value = ''"
      />
    </ChatFooter>
  </Chat>
</template>

An opt-in light palette

.newt-light and [data-newt-theme="light"] override the surface, text, status and shadow tokens for light backgrounds. Nothing changes for anyone who never asks for it: the dark values stay the default in :root.

Four tokens are new — --newt-bg-input-elevated, --newt-mention-text, --newt-mention-role and --newt-embed-max-width — and the hex values that had been duplicated across the mention, badge, timeline, reaction and slash-command components now read those tokens instead.

Reworked components

  • Mention renders a real button with focus styles, and gains an optional avatar, an app variant and a color prop for role mentions.
  • Button gains emoji and the link-out glyph, on by default for variant="link".
  • Embed gains an accent colour, an author row, a footer icon, a middle-dot separator and a <time> timestamp.
  • MemberList gains role sections with headings, an offline state, coloured names and the APP tag with its verified check.
  • ReplyPreview gains an avatar, a “used” action and a slash-command chip; the spine is driven by custom properties.
  • MessageGroup gains a reply grid layout, an ephemeral state with its notice, the verified bot tag, and renders its timestamp as <time>.
  • Divider gains spacing and line; ScrollArea gains focusable and viewportLabel.
  • SlashCommand now takes the full invocation: the subcommand path plus typed options, including the focused one.
  • SelectMenu is a complete combobox: a trigger, a keyboard-driven listbox and a portalled panel that flips when it runs out of space.
Root class newt-select-menu
"use client"

import * as React from "react"

import { SelectMenu } from "@/registry/bases/newt/ui/select-menu"

/*
 * Drawn here rather than pulled from an icon package: the registry ships no
 * runtime dependency, and a device picker needs exactly two glyphs.
 */
const Monitor = () => (
  <svg viewBox="0 0 24 24" className="h-full w-full" aria-hidden="true">
    <path
      fill="currentColor"
      d="M4 5h16a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-6v2h3a1 1 0 1 1 0 2H7a1 1 0 1 1 0-2h3v-2H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm1 2v7h14V7H5Z"
    />
  </svg>
)

const Headset = () => (
  <svg viewBox="0 0 24 24" className="h-full w-full" aria-hidden="true">
    <path
      fill="currentColor"
      d="M12 3a8 8 0 0 0-8 8v6a3 3 0 0 0 3 3h1a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H6v-1a6 6 0 1 1 12 0v1h-2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a3 3 0 0 0 3-3v-6a8 8 0 0 0-8-8Z"
    />
  </svg>
)

const OPTIONS = [
  {
    value: "default",
    label: "System Default",
    description: "Speakers (Built-in Output)",
    note: "(Speakers)",
    icon: <Monitor />,
  },
  {
    value: "speakers",
    label: "Speakers",
    description: "Built-in Output",
    icon: <Monitor />,
  },
  {
    value: "headset",
    label: "Headset",
    description: "USB Audio Device",
    icon: <Headset />,
  },
  {
    value: "bluetooth",
    label: "Bluetooth Headset",
    description: "Not connected",
    icon: <Headset />,
    disabled: true,
  },
]

export default function SelectMenuDemo() {
  const [value, setValue] = React.useState<string>("default")

  return (
    <SelectMenu
      options={OPTIONS}
      value={value}
      onValueChange={setValue}
      label="Output device"
    />
  )
}
<script setup lang="ts">
import { h, markRaw, ref } from "vue"
import {
  SelectMenu,
  type SelectMenuOption,
} from "~~/registry/bases/newt/ui/select-menu"

/*
 * Drawn here rather than pulled from an icon package: the registry ships no
 * runtime dependency, and a device picker needs exactly two glyphs.
 */
const Monitor = markRaw({
  render: () =>
    h(
      "svg",
      { viewBox: "0 0 24 24", class: "h-full w-full", "aria-hidden": "true" },
      [
        h("path", {
          fill: "currentColor",
          d: "M4 5h16a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-6v2h3a1 1 0 1 1 0 2H7a1 1 0 1 1 0-2h3v-2H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm1 2v7h14V7H5Z",
        }),
      ]
    ),
})

const Headset = markRaw({
  render: () =>
    h(
      "svg",
      { viewBox: "0 0 24 24", class: "h-full w-full", "aria-hidden": "true" },
      [
        h("path", {
          fill: "currentColor",
          d: "M12 3a8 8 0 0 0-8 8v6a3 3 0 0 0 3 3h1a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H6v-1a6 6 0 1 1 12 0v1h-2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a3 3 0 0 0 3-3v-6a8 8 0 0 0-8-8Z",
        }),
      ]
    ),
})

const OPTIONS: SelectMenuOption[] = [
  {
    value: "default",
    label: "System Default",
    description: "Speakers (Built-in Output)",
    note: "(Speakers)",
    icon: Monitor,
  },
  {
    value: "speakers",
    label: "Speakers",
    description: "Built-in Output",
    icon: Monitor,
  },
  {
    value: "headset",
    label: "Headset",
    description: "USB Audio Device",
    icon: Headset,
  },
  {
    value: "bluetooth",
    label: "Bluetooth Headset",
    description: "Not connected",
    icon: Headset,
    disabled: true,
  },
]

const value = ref("default")
</script>

<template>
  <SelectMenu v-model="value" :options="OPTIONS" label="Output device" />
</template>