Skip to content

newt/uiv0.3.0

Discord-inspired UI, copy-pasted into your project.

A component library, a design token system and an agent guide for building Discord-inspired interfaces — bots, dashboards and docs that look like they belong in the client. Every component ships as plain HTML and CSS, as a React file, and as a Vue single file component, all reading one set of tokens.

components
71
runtime deps
0
license
Apache-2.0
install
npx newtui
tokens
CSS vars
frameworks
React · Vue · HTML

Install

One CLI for both frameworks. It reads your project during init, then writes component source into it — there is no package between you and the markup.

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

Three of them, running

The same examples every component page carries, rendered from the registry source in the framework the header has chosen.

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>
Root class newt-member
import { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
  MemberList,
  MemberListAppTag,
  MemberListHeading,
  MemberListInfo,
  MemberListItem,
  MemberListName,
  MemberListNameRow,
  MemberListRole,
  MemberListSection,
} from "@/registry/bases/newt/ui/member-list"
import { RoleTag } from "@/registry/bases/newt/ui/role-tag"
import {
  StatusDot,
  StatusIndicator,
} from "@/registry/bases/newt/ui/status-indicator"

export default function MemberListDemo() {
  return (
    <MemberList className="w-60">
      <MemberListSection>
        <MemberListHeading>Moderators — 1</MemberListHeading>
        <MemberListItem>
          <StatusIndicator>
            <Avatar size="sm">A</Avatar>
            <StatusDot status="online" />
          </StatusIndicator>
          <MemberListInfo>
            <MemberListNameRow>
              <MemberListName color="#f0b232">username</MemberListName>
              <MemberListAppTag verified />
            </MemberListNameRow>
            <MemberListRole>Role</MemberListRole>
          </MemberListInfo>
          <RoleTag className="ml-auto">TAG</RoleTag>
        </MemberListItem>
      </MemberListSection>
      <MemberListSection offline>
        <MemberListHeading>Offline — 1</MemberListHeading>
        <MemberListItem>
          <StatusIndicator>
            <Avatar size="sm">B</Avatar>
            <StatusDot status="offline" />
          </StatusIndicator>
          <MemberListInfo>
            <MemberListNameRow>
              <MemberListName>someone</MemberListName>
            </MemberListNameRow>
          </MemberListInfo>
        </MemberListItem>
      </MemberListSection>
    </MemberList>
  )
}
<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
  MemberList,
  MemberListAppTag,
  MemberListHeading,
  MemberListInfo,
  MemberListItem,
  MemberListName,
  MemberListNameRow,
  MemberListRole,
  MemberListSection,
} from "~~/registry/bases/newt/ui/member-list"
import { RoleTag } from "~~/registry/bases/newt/ui/role-tag"
import {
  StatusDot,
  StatusIndicator,
} from "~~/registry/bases/newt/ui/status-indicator"
</script>

<template>
  <MemberList class="w-60">
    <MemberListSection>
      <MemberListHeading>Moderators — 1</MemberListHeading>
      <MemberListItem>
        <StatusIndicator>
          <Avatar size="sm">A</Avatar>
          <StatusDot status="online" />
        </StatusIndicator>
        <MemberListInfo>
          <MemberListNameRow>
            <MemberListName color="#f0b232">username</MemberListName>
            <MemberListAppTag verified />
          </MemberListNameRow>
          <MemberListRole>Role</MemberListRole>
        </MemberListInfo>
        <RoleTag class="ml-auto">TAG</RoleTag>
      </MemberListItem>
    </MemberListSection>
    <MemberListSection offline>
      <MemberListHeading>Offline — 1</MemberListHeading>
      <MemberListItem>
        <StatusIndicator>
          <Avatar size="sm">B</Avatar>
          <StatusDot status="offline" />
        </StatusIndicator>
        <MemberListInfo>
          <MemberListNameRow>
            <MemberListName>someone</MemberListName>
          </MemberListNameRow>
        </MemberListInfo>
      </MemberListItem>
    </MemberListSection>
  </MemberList>
</template>
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>

What it is

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.

Where to go next

Start with the installation guide, browse the components, or read how to drive the registry from an agent in Using with AI. Issues and the registry source are on GitHub.