Skip to content

Introduction

A component library, a design token system and an agent guide for building Discord-inspired interfaces in React, Vue and plain HTML.

newt/ui is a component library for building Discord-inspired interfaces — bots, dashboards and docs that look like they belong next to the client. Every component ships three times over: as plain HTML and CSS, as a React file, and as a Vue single file component, all reading one set of --newt-* design tokens.

There is no package to install and no runtime to depend on. The CLI copies component source into your project, the same way shadcn/ui does, and stops there.

Root class newt-message-group
import { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupBotTag,
  MessageGroupContent,
  MessageGroupEphemeralAction,
  MessageGroupEphemeralNotice,
  MessageGroupHeader,
  MessageGroupReactions,
  MessageGroupReply,
  MessageGroupTime,
} from "@/registry/bases/newt/ui/message-group"
import { Reaction } from "@/registry/bases/newt/ui/reaction"
import {
  ReplyPreview,
  ReplyPreviewAuthor,
  ReplyPreviewText,
} from "@/registry/bases/newt/ui/reply-preview"
import {
  StatusDot,
  StatusIndicator,
} from "@/registry/bases/newt/ui/status-indicator"

export default function MessageGroupDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-2">
      <MessageGroup withReply>
        <MessageGroupReply>
          <ReplyPreview>
            <ReplyPreviewAuthor>@someone</ReplyPreviewAuthor>
            <ReplyPreviewText>original message text</ReplyPreviewText>
          </ReplyPreview>
        </MessageGroupReply>
        <MessageGroupAvatar>
          <StatusIndicator>
            <Avatar>U</Avatar>
            <StatusDot status="online" />
          </StatusIndicator>
        </MessageGroupAvatar>
        <MessageGroupBody>
          <MessageGroupHeader>
            <MessageGroupAuthor>username</MessageGroupAuthor>
            <MessageGroupBotTag verified />
            <MessageGroupTime dateTime="2026-09-04T14:23:00.000Z">
              Today at 14:23
            </MessageGroupTime>
          </MessageGroupHeader>
          <MessageGroupContent>Message content goes here.</MessageGroupContent>
          <MessageGroupReactions>
            <Reaction emoji="👍" count={1} />
          </MessageGroupReactions>
        </MessageGroupBody>
      </MessageGroup>
      <MessageGroup ephemeral>
        <MessageGroupAvatar>
          <Avatar>B</Avatar>
        </MessageGroupAvatar>
        <MessageGroupBody>
          <MessageGroupHeader>
            <MessageGroupAuthor>botname</MessageGroupAuthor>
            <MessageGroupBotTag>APP</MessageGroupBotTag>
          </MessageGroupHeader>
          <MessageGroupContent>Command ran successfully.</MessageGroupContent>
          <MessageGroupEphemeralNotice>
            Only you can see this •
            <MessageGroupEphemeralAction>
              Dismiss message
            </MessageGroupEphemeralAction>
          </MessageGroupEphemeralNotice>
        </MessageGroupBody>
      </MessageGroup>
    </div>
  )
}
<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
  MessageGroup,
  MessageGroupAuthor,
  MessageGroupAvatar,
  MessageGroupBody,
  MessageGroupBotTag,
  MessageGroupContent,
  MessageGroupEphemeralAction,
  MessageGroupEphemeralNotice,
  MessageGroupHeader,
  MessageGroupReactions,
  MessageGroupReply,
  MessageGroupTime,
} from "~~/registry/bases/newt/ui/message-group"
import { Reaction } from "~~/registry/bases/newt/ui/reaction"
import {
  ReplyPreview,
  ReplyPreviewAuthor,
  ReplyPreviewText,
} from "~~/registry/bases/newt/ui/reply-preview"
import {
  StatusDot,
  StatusIndicator,
} from "~~/registry/bases/newt/ui/status-indicator"
</script>

<template>
  <div class="flex w-full max-w-md flex-col gap-2">
    <MessageGroup with-reply>
      <MessageGroupReply>
        <ReplyPreview>
          <ReplyPreviewAuthor>@someone</ReplyPreviewAuthor>
          <ReplyPreviewText>original message text</ReplyPreviewText>
        </ReplyPreview>
      </MessageGroupReply>
      <MessageGroupAvatar>
        <StatusIndicator>
          <Avatar>U</Avatar>
          <StatusDot status="online" />
        </StatusIndicator>
      </MessageGroupAvatar>
      <MessageGroupBody>
        <MessageGroupHeader>
          <MessageGroupAuthor>username</MessageGroupAuthor>
          <MessageGroupBotTag verified />
          <MessageGroupTime datetime="2026-09-04T14:23:00.000Z">
            Today at 14:23
          </MessageGroupTime>
        </MessageGroupHeader>
        <MessageGroupContent>Message content goes here.</MessageGroupContent>
        <MessageGroupReactions>
          <Reaction emoji="👍" :count="1" />
        </MessageGroupReactions>
      </MessageGroupBody>
    </MessageGroup>
    <MessageGroup ephemeral>
      <MessageGroupAvatar>
        <Avatar>B</Avatar>
      </MessageGroupAvatar>
      <MessageGroupBody>
        <MessageGroupHeader>
          <MessageGroupAuthor>botname</MessageGroupAuthor>
          <MessageGroupBotTag>APP</MessageGroupBotTag>
        </MessageGroupHeader>
        <MessageGroupContent>Command ran successfully.</MessageGroupContent>
        <MessageGroupEphemeralNotice>
          Only you can see this •
          <MessageGroupEphemeralAction>
            Dismiss message
          </MessageGroupEphemeralAction>
        </MessageGroupEphemeralNotice>
      </MessageGroupBody>
    </MessageGroup>
  </div>
</template>

What it is

Four decisions the whole registry is built on. They are why a component here is short enough to read before you paste it.

One token system, three targets

The tokens are declared once and every surface reads them: the plain HTML and CSS build, the React wrappers, and the Vue single file components. Change a token and all three move together.

You own the code

The CLI writes component source into your project and stops there. A component you need to bend is a file you already have, under Apache-2.0.

No motion by default

Hover, focus, online, do-not-disturb and loading are carried by colour, border and shape — the status dot states its mode by its outline, not by a pulse.

Accessible by construction

Body text meets WCAG AA contrast, every control is reachable by Tab with a visible focus ring, and presence is announced rather than left to colour alone.

Install

One CLI for both frameworks. It reads your project during init, then writes component source into it.

pnpm dlx newtui@latest init
npx newtui@latest init
yarn dlx newtui@latest init
bunx newtui@latest init

Then add what you need:

pnpm dlx newtui@latest add button embed member-list
npx newtui@latest add button embed member-list
yarn dlx newtui@latest add button embed member-list
bunx newtui@latest add button embed member-list

Where to go next

Start with the installation guide, which covers both CLIs and the Nuxt module; browse the components, each with a live example, the files it writes and the dependencies it needs; or read how to drive the registry from an agent in Using with AI.

newt/ui is an independent project. It is visually inspired by Discord’s client design but is not affiliated with, endorsed by, or sponsored by Discord Inc.