Skip to content

System Message

The lines a channel writes about itself — joins, boosts, pins — with no author and no avatar.

The lines a channel writes about itself — joins, boosts, pins — with no author and no avatar. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

import {
  SystemMessage,
  SystemMessageActor,
  SystemMessageTime,
} from "@/registry/bases/newt/ui/system-message"

export default function SystemMessageDemo() {
  return (
    <div className="flex w-full max-w-[420px] flex-col gap-1">
      <SystemMessage kind="join">
        <SystemMessageActor>someone</SystemMessageActor> joined the channel.{" "}
        <SystemMessageTime>Today at 09:41</SystemMessageTime>
      </SystemMessage>
      <SystemMessage kind="boost">
        <SystemMessageActor>another</SystemMessageActor> boosted the server.{" "}
        <SystemMessageTime>Today at 10:02</SystemMessageTime>
      </SystemMessage>
      <SystemMessage kind="pin">
        <SystemMessageActor>a moderator</SystemMessageActor> pinned a message.{" "}
        <SystemMessageTime>Today at 10:15</SystemMessageTime>
      </SystemMessage>
    </div>
  )
}
<script setup lang="ts">
import {
  SystemMessage,
  SystemMessageActor,
  SystemMessageTime,
} from "~~/registry/bases/newt/ui/system-message"
</script>

<template>
  <div class="flex w-full max-w-[420px] flex-col gap-1">
    <SystemMessage kind="join">
      <SystemMessageActor>someone</SystemMessageActor> joined the channel.
      <SystemMessageTime>Today at 09:41</SystemMessageTime>
    </SystemMessage>
    <SystemMessage kind="boost">
      <SystemMessageActor>another</SystemMessageActor> boosted the server.
      <SystemMessageTime>Today at 10:02</SystemMessageTime>
    </SystemMessage>
    <SystemMessage kind="pin">
      <SystemMessageActor>a moderator</SystemMessageActor> pinned a message.
      <SystemMessageTime>Today at 10:15</SystemMessageTime>
    </SystemMessage>
  </div>
</template>

Installation

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

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy the source into your project

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

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

/*
 * The lines a channel writes about itself: somebody joined, somebody boosted,
 * a message was pinned. They sit in the timeline where a message would, but
 * they have no author and no avatar — the icon carries the whole meaning, and
 * the colour says which kind of event it was.
 */
const systemMessageVariants = cva("shrink-0", {
  variants: {
    kind: {
      join: "text-newt-online",
      leave: "text-newt-dnd",
      boost: "text-[#ff73fa]",
      pin: "text-newt-text-muted",
      call: "text-newt-online",
    },
  },
  defaultVariants: { kind: "join" },
})

/*
 * Drawn here rather than pulled from an icon set: the registry ships no
 * runtime dependency, and each of these is one shape.
 */
const ICONS = {
  join: "M12 4a1 1 0 0 1 1 1v6h6a1 1 0 1 1 0 2h-6v6a1 1 0 1 1-2 0v-6H5a1 1 0 1 1 0-2h6V5a1 1 0 0 1 1-1Z",
  leave:
    "M7.05 5.64a1 1 0 0 1 1.41 0L12 9.17l3.54-3.53a1 1 0 1 1 1.41 1.41L13.41 10.6l3.54 3.53a1 1 0 0 1-1.41 1.42L12 12.01l-3.54 3.54a1 1 0 0 1-1.41-1.42l3.53-3.53-3.53-3.54a1 1 0 0 1 0-1.42Z",
  boost:
    "M12 2.5 14.8 8l6.2.9-4.5 4.3 1.1 6.1-5.6-3-5.6 3 1.1-6.1L3 8.9 9.2 8 12 2.5Z",
  pin: "M14.5 2.5a1 1 0 0 1 1.7.7v4.3l3.1 3.1a1 1 0 0 1-.7 1.7h-5.2l-1.7 6.9a1 1 0 0 1-1.9.1L8 12.3H3.8a1 1 0 0 1-.7-1.7l3.1-3.1V3.2a1 1 0 0 1 1.7-.7h6.6Z",
  call: "M6.6 3a1 1 0 0 1 .9.6l1.4 3a1 1 0 0 1-.3 1.2L7.2 8.9a11 11 0 0 0 4.9 4.9l1.1-1.4a1 1 0 0 1 1.2-.3l3 1.4a1 1 0 0 1 .6.9v2.9a1 1 0 0 1-1.1 1A15.4 15.4 0 0 1 3.6 4.1 1 1 0 0 1 4.6 3h2Z",
} as const satisfies Record<
  NonNullable<VariantProps<typeof systemMessageVariants>["kind"]>,
  string
>

export interface SystemMessageProps
  extends
    React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof systemMessageVariants> {
  /** Replaces the drawn glyph, for an event this list does not cover. */
  icon?: React.ReactNode
}

const SystemMessage = React.forwardRef<HTMLDivElement, SystemMessageProps>(
  ({ className, kind = "join", icon, children, ...props }, ref) => (
    <div
      ref={ref}
      className={cn(
        "flex items-center gap-3 px-2 py-1 text-sm leading-[1.375] text-newt-text-muted",
        className
      )}
      {...props}
    >
      <span
        aria-hidden="true"
        className={cn(
          "flex h-4 w-4 items-center justify-center",
          systemMessageVariants({ kind })
        )}
      >
        {icon ?? (
          <svg viewBox="0 0 24 24" className="h-full w-full">
            <path fill="currentColor" d={ICONS[kind ?? "join"]} />
          </svg>
        )}
      </span>
      <span className="min-w-0">{children}</span>
    </div>
  )
)
SystemMessage.displayName = "SystemMessage"

/** The name inside a system line, which is bolder than the sentence it sits in. */
const SystemMessageActor = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn("font-medium text-newt-text-primary", className)}
    {...props}
  />
))
SystemMessageActor.displayName = "SystemMessageActor"

const SystemMessageTime = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn("text-xs text-newt-text-muted", className)}
    {...props}
  />
))
SystemMessageTime.displayName = "SystemMessageTime"

export {
  SystemMessage,
  SystemMessageActor,
  SystemMessageTime,
  systemMessageVariants,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

import {
  systemMessageIcons,
  systemMessageVariants,
  type SystemMessageKind,
} from "."

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    kind?: SystemMessageKind
  }>(),
  { kind: "join" }
)
</script>

<template>
  <div
    :class="
      cn(
        'flex items-center gap-3 px-2 py-1 text-sm leading-[1.375] text-newt-text-muted',
        props.class
      )
    "
  >
    <span
      aria-hidden="true"
      :class="
        cn(
          'flex h-4 w-4 items-center justify-center',
          systemMessageVariants({ kind: props.kind })
        )
      "
    >
      <!-- `icon` replaces the drawn glyph, for an event this list misses. -->
      <slot name="icon">
        <svg viewBox="0 0 24 24" class="h-full w-full">
          <path fill="currentColor" :d="systemMessageIcons[props.kind]" />
        </svg>
      </slot>
    </span>
    <span class="min-w-0"><slot /></span>
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

/** The name inside a system line, bolder than the sentence it sits in. */
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span :class="cn('font-medium text-newt-text-primary', props.class)">
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span :class="cn('text-xs text-newt-text-muted', props.class)">
    <slot />
  </span>
</template>
import { cva, type VariantProps } from "class-variance-authority"

export { default as SystemMessage } from "./SystemMessage.vue"
export { default as SystemMessageActor } from "./SystemMessageActor.vue"
export { default as SystemMessageTime } from "./SystemMessageTime.vue"

/*
 * The lines a channel writes about itself: somebody joined, somebody boosted,
 * a message was pinned. The icon carries the meaning; the colour says which
 * kind of event it was.
 */
export const systemMessageVariants = cva("shrink-0", {
  variants: {
    kind: {
      join: "text-newt-online",
      leave: "text-newt-dnd",
      boost: "text-[#ff73fa]",
      pin: "text-newt-text-muted",
      call: "text-newt-online",
    },
  },
  defaultVariants: { kind: "join" },
})

export type SystemMessageVariants = VariantProps<typeof systemMessageVariants>

export type SystemMessageKind = NonNullable<SystemMessageVariants["kind"]>

/*
 * Drawn here rather than pulled from an icon set: the registry ships no
 * runtime dependency, and each of these is one shape.
 */
export const systemMessageIcons = {
  join: "M12 4a1 1 0 0 1 1 1v6h6a1 1 0 1 1 0 2h-6v6a1 1 0 1 1-2 0v-6H5a1 1 0 1 1 0-2h6V5a1 1 0 0 1 1-1Z",
  leave:
    "M7.05 5.64a1 1 0 0 1 1.41 0L12 9.17l3.54-3.53a1 1 0 1 1 1.41 1.41L13.41 10.6l3.54 3.53a1 1 0 0 1-1.41 1.42L12 12.01l-3.54 3.54a1 1 0 0 1-1.41-1.42l3.53-3.53-3.53-3.54a1 1 0 0 1 0-1.42Z",
  boost:
    "M12 2.5 14.8 8l6.2.9-4.5 4.3 1.1 6.1-5.6-3-5.6 3 1.1-6.1L3 8.9 9.2 8 12 2.5Z",
  pin: "M14.5 2.5a1 1 0 0 1 1.7.7v4.3l3.1 3.1a1 1 0 0 1-.7 1.7h-5.2l-1.7 6.9a1 1 0 0 1-1.9.1L8 12.3H3.8a1 1 0 0 1-.7-1.7l3.1-3.1V3.2a1 1 0 0 1 1.7-.7h6.6Z",
  call: "M6.6 3a1 1 0 0 1 .9.6l1.4 3a1 1 0 0 1-.3 1.2L7.2 8.9a11 11 0 0 0 4.9 4.9l1.1-1.4a1 1 0 0 1 1.2-.3l3 1.4a1 1 0 0 1 .6.9v2.9a1 1 0 0 1-1.1 1A15.4 15.4 0 0 1 3.6 4.1 1 1 0 0 1 4.6 3h2Z",
} as const satisfies Record<SystemMessageKind, string>

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

export function Example() {
  return <SystemMessage />
}
<script setup lang="ts">
import { SystemMessage, SystemMessageActor, SystemMessageTime } from "@/components/ui/system-message"
</script>

<template>
  <SystemMessage />
</template>

API reference

SystemMessage

PropType
children / default slot?React.ReactNode / Vue slot

Content rendered inside the component.

TypeReact.ReactNode / Vue slot
className / class?string

Additional classes merged with the component defaults.

Typestring

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

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