Skip to content

Embed

A Discord-inspired rich message embed with an accent bar, author, title, description, fields, footer and timestamp.

A Discord-inspired rich message embed with an accent bar, author, title, description, fields, footer and timestamp. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-embed
import {
  Embed,
  EmbedAuthor,
  EmbedAuthorIcon,
  EmbedAuthorName,
  EmbedDescription,
  EmbedEyebrow,
  EmbedField,
  EmbedFieldName,
  EmbedFieldValue,
  EmbedFields,
  EmbedFooter,
  EmbedFooterIcon,
  EmbedFooterSeparator,
  EmbedTimestamp,
  EmbedTitle,
} from "@/registry/bases/newt/ui/embed"

/* Inline so the demo never reaches out to the network for a placeholder. */
const ICON =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%2323a55a'/%3E%3C/svg%3E"

export default function EmbedDemo() {
  return (
    <Embed color="#23a55a">
      <EmbedAuthor>
        <EmbedAuthorIcon src={ICON} />
        <EmbedAuthorName>Author name</EmbedAuthorName>
      </EmbedAuthor>
      <EmbedEyebrow>Source · category</EmbedEyebrow>
      <EmbedTitle>Embed title</EmbedTitle>
      <EmbedDescription>Embed description text goes here.</EmbedDescription>
      <EmbedFields>
        <EmbedField>
          <EmbedFieldName>Field</EmbedFieldName>
          <EmbedFieldValue>Value</EmbedFieldValue>
        </EmbedField>
      </EmbedFields>
      <EmbedFooter>
        <EmbedFooterIcon src={ICON} />
        <span>
          Footer text
          <EmbedFooterSeparator />
          <EmbedTimestamp date="2026-09-04T12:00:00.000Z" />
        </span>
      </EmbedFooter>
    </Embed>
  )
}
<script setup lang="ts">
import {
  Embed,
  EmbedAuthor,
  EmbedAuthorIcon,
  EmbedAuthorName,
  EmbedDescription,
  EmbedEyebrow,
  EmbedField,
  EmbedFieldName,
  EmbedFieldValue,
  EmbedFields,
  EmbedFooter,
  EmbedFooterIcon,
  EmbedFooterSeparator,
  EmbedTimestamp,
  EmbedTitle,
} from "~~/registry/bases/newt/ui/embed"

/* Inline so the demo never reaches out to the network for a placeholder. */
const ICON =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%2323a55a'/%3E%3C/svg%3E"
</script>

<template>
  <Embed color="#23a55a">
    <EmbedAuthor>
      <EmbedAuthorIcon :src="ICON" />
      <EmbedAuthorName>Author name</EmbedAuthorName>
    </EmbedAuthor>
    <EmbedEyebrow>Source · category</EmbedEyebrow>
    <EmbedTitle>Embed title</EmbedTitle>
    <EmbedDescription>Embed description text goes here.</EmbedDescription>
    <EmbedFields>
      <EmbedField>
        <EmbedFieldName>Field</EmbedFieldName>
        <EmbedFieldValue>Value</EmbedFieldValue>
      </EmbedField>
    </EmbedFields>
    <EmbedFooter>
      <EmbedFooterIcon :src="ICON" />
      <span>
        Footer text
        <EmbedFooterSeparator />
        <EmbedTimestamp date="2026-09-04T12:00:00.000Z" />
      </span>
    </EmbedFooter>
  </Embed>
</template>

Installation

pnpm dlx newtui@latest add embed
npx newtui@latest add embed
yarn dlx newtui@latest add embed
bunx newtui@latest add embed

Copy the source into your project

import * as React from "react"

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

export interface EmbedProps extends Omit<React.ComponentProps<"div">, "color"> {
  /** Accent colour of the left bar. Any CSS colour; defaults to the brand. */
  color?: string
}

const Embed = React.forwardRef<HTMLDivElement, EmbedProps>(
  ({ className, children, color, style, ...props }, ref) => (
    <div
      ref={ref}
      /*
       * SAFETY: `CSSProperties` has no index signature for custom properties,
       * but React forwards unknown `--*` keys to the style attribute verbatim,
       * so the extra key is written out exactly as spelled here.
       */
      style={
        color
          ? ({ ...style, "--newt-embed-color": color } as React.CSSProperties)
          : style
      }
      className={cn(
        "flex max-w-[var(--newt-embed-max-width)] overflow-hidden rounded-md border-s-4 border-[var(--newt-embed-color,var(--newt-brand))] bg-newt-bg-elevated",
        className
      )}
      {...props}
    >
      <div className="flex min-w-0 flex-1 flex-col gap-2 px-4 py-3">
        {children}
      </div>
    </div>
  )
)
Embed.displayName = "Embed"

const EmbedAuthor = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex min-w-0 items-center gap-2 text-[14px] font-semibold text-newt-text-primary",
      className
    )}
    {...props}
  />
))
EmbedAuthor.displayName = "EmbedAuthor"

const EmbedAuthorIcon = React.forwardRef<
  HTMLImageElement,
  React.ComponentProps<"img">
>(({ className, alt = "", ...props }, ref) => (
  <img
    ref={ref}
    alt={alt}
    aria-hidden={alt === "" ? true : undefined}
    className={cn("h-6 w-6 shrink-0 rounded-full object-cover", className)}
    {...props}
  />
))
EmbedAuthorIcon.displayName = "EmbedAuthorIcon"

const EmbedAuthorName = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
  <span ref={ref} className={cn("truncate", className)} {...props} />
))
EmbedAuthorName.displayName = "EmbedAuthorName"

const EmbedEyebrow = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "text-[12px] font-semibold text-newt-text-secondary",
      className
    )}
    {...props}
  />
))
EmbedEyebrow.displayName = "EmbedEyebrow"

const EmbedTitle = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-[16px] font-semibold text-newt-text-link", className)}
    {...props}
  />
))
EmbedTitle.displayName = "EmbedTitle"

const EmbedDescription = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "text-[14px] leading-[20px] text-newt-text-secondary",
      className
    )}
    {...props}
  />
))
EmbedDescription.displayName = "EmbedDescription"

const EmbedFields = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mt-1 grid grid-cols-[repeat(auto-fit,minmax(120px,1fr))] gap-2",
      className
    )}
    {...props}
  />
))
EmbedFields.displayName = "EmbedFields"

const EmbedField = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn(className)} {...props} />
))
EmbedField.displayName = "EmbedField"

const EmbedFieldName = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mb-0.5 text-[13px] font-semibold text-newt-text-primary",
      className
    )}
    {...props}
  />
))
EmbedFieldName.displayName = "EmbedFieldName"

const EmbedFieldValue = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-[13px] text-newt-text-secondary", className)}
    {...props}
  />
))
EmbedFieldValue.displayName = "EmbedFieldValue"

const EmbedFooter = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "mt-1 flex min-w-0 items-center gap-2 text-[12px] text-newt-text-muted",
      className
    )}
    {...props}
  />
))
EmbedFooter.displayName = "EmbedFooter"

const EmbedFooterIcon = React.forwardRef<
  HTMLImageElement,
  React.ComponentProps<"img">
>(({ className, alt = "", ...props }, ref) => (
  <img
    ref={ref}
    alt={alt}
    aria-hidden={alt === "" ? true : undefined}
    className={cn("h-4 w-4 shrink-0 rounded-full object-cover", className)}
    {...props}
  />
))
EmbedFooterIcon.displayName = "EmbedFooterIcon"

/* Middle dot, the way Discord separates footer text from the timestamp. */
const EmbedFooterSeparator = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, children = "\u2022", ...props }, ref) => (
  <span
    ref={ref}
    aria-hidden="true"
    className={cn("mx-1", className)}
    {...props}
  >
    {children}
  </span>
))
EmbedFooterSeparator.displayName = "EmbedFooterSeparator"

/*
 * A fixed locale, not the visitor's: an embed rendered on the server and
 * hydrated in the browser has to produce the same string in both places.
 */
const embedTimeFormat = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short",
  timeStyle: "short",
})

export interface EmbedTimestampProps extends Omit<
  React.ComponentProps<"time">,
  "dateTime"
> {
  /** The moment to render. Anything `new Date()` accepts. */
  date: Date | number | string
}

const EmbedTimestamp = React.forwardRef<HTMLTimeElement, EmbedTimestampProps>(
  ({ className, date, children, ...props }, ref) => {
    const value = date instanceof Date ? date : new Date(date)
    const valid = !Number.isNaN(value.getTime())
    return (
      <time
        ref={ref}
        dateTime={valid ? value.toISOString() : undefined}
        className={cn(className)}
        {...props}
      >
        {children ?? (valid ? embedTimeFormat.format(value) : "")}
      </time>
    )
  }
)
EmbedTimestamp.displayName = "EmbedTimestamp"

export {
  Embed,
  EmbedAuthor,
  EmbedAuthorIcon,
  EmbedAuthorName,
  EmbedEyebrow,
  EmbedTitle,
  EmbedDescription,
  EmbedFields,
  EmbedField,
  EmbedFieldName,
  EmbedFieldValue,
  EmbedFooter,
  EmbedFooterIcon,
  EmbedFooterSeparator,
  EmbedTimestamp,
}
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from "vue"
import { computed } from "vue"

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

const props = defineProps<{
  class?: HTMLAttributes["class"]
  /** Accent colour of the left bar. Any CSS colour; defaults to the brand. */
  color?: string
}>()

/*
 * SAFETY: `CSSProperties` has no index signature for custom properties, but
 * Vue writes unknown `--*` keys to the style attribute verbatim, so the extra
 * key lands exactly as spelled here.
 */
const style = computed<CSSProperties | undefined>(() =>
  props.color
    ? ({ "--newt-embed-color": props.color } as CSSProperties)
    : undefined
)
</script>

<template>
  <div
    :style="style"
    :class="
      cn(
        'flex max-w-[var(--newt-embed-max-width)] overflow-hidden rounded-md border-s-4 border-[var(--newt-embed-color,var(--newt-brand))] bg-newt-bg-elevated',
        props.class
      )
    "
  >
    <div class="flex min-w-0 flex-1 flex-col gap-2 px-4 py-3">
      <slot />
    </div>
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="
      cn(
        'flex min-w-0 items-center gap-2 text-[14px] font-semibold text-newt-text-primary',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = withDefaults(
  defineProps<{ class?: HTMLAttributes["class"]; src: string; alt?: string }>(),
  { alt: "" }
)
</script>

<template>
  <img
    :src="props.src"
    :alt="props.alt"
    :aria-hidden="props.alt === '' ? true : undefined"
    :class="cn('h-6 w-6 shrink-0 rounded-full object-cover', props.class)"
  />
</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('truncate', 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>
  <div
    :class="
      cn('text-[12px] font-semibold text-newt-text-secondary', props.class)
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="cn('text-[16px] font-semibold text-newt-text-link', props.class)"
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="
      cn('text-[14px] leading-[20px] text-newt-text-secondary', props.class)
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="
      cn(
        'mt-1 grid grid-cols-[repeat(auto-fit,minmax(120px,1fr))] gap-2',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div :class="cn('', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="
      cn('mb-0.5 text-[13px] font-semibold text-newt-text-primary', props.class)
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div :class="cn('text-[13px] text-newt-text-secondary', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<template>
  <div
    :class="
      cn(
        'mt-1 flex min-w-0 items-center gap-2 text-[12px] text-newt-text-muted',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

const props = withDefaults(
  defineProps<{ class?: HTMLAttributes["class"]; src: string; alt?: string }>(),
  { alt: "" }
)
</script>

<template>
  <img
    :src="props.src"
    :alt="props.alt"
    :aria-hidden="props.alt === '' ? true : undefined"
    :class="cn('h-4 w-4 shrink-0 rounded-full object-cover', props.class)"
  />
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

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

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

<!-- Middle dot, the way Discord separates footer text from the timestamp. -->
<template>
  <span aria-hidden="true" :class="cn('mx-1', props.class)">
    <slot>&bull;</slot>
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"

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

const props = defineProps<{
  class?: HTMLAttributes["class"]
  /** The moment to render. Anything `new Date()` accepts. */
  date: Date | number | string
}>()

/*
 * A fixed locale, not the visitor's: an embed rendered on the server and
 * hydrated in the browser has to produce the same string in both places.
 */
const formatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short",
  timeStyle: "short",
})

const value = computed(() =>
  props.date instanceof Date ? props.date : new Date(props.date)
)
const valid = computed(() => !Number.isNaN(value.value.getTime()))
</script>

<template>
  <time
    :datetime="valid ? value.toISOString() : undefined"
    :class="cn(props.class)"
  >
    <slot>{{ valid ? formatter.format(value) : "" }}</slot>
  </time>
</template>
export { default as Embed } from "./Embed.vue"
export { default as EmbedAuthor } from "./EmbedAuthor.vue"
export { default as EmbedAuthorIcon } from "./EmbedAuthorIcon.vue"
export { default as EmbedAuthorName } from "./EmbedAuthorName.vue"
export { default as EmbedEyebrow } from "./EmbedEyebrow.vue"
export { default as EmbedTitle } from "./EmbedTitle.vue"
export { default as EmbedDescription } from "./EmbedDescription.vue"
export { default as EmbedFields } from "./EmbedFields.vue"
export { default as EmbedField } from "./EmbedField.vue"
export { default as EmbedFieldName } from "./EmbedFieldName.vue"
export { default as EmbedFieldValue } from "./EmbedFieldValue.vue"
export { default as EmbedFooter } from "./EmbedFooter.vue"
export { default as EmbedFooterIcon } from "./EmbedFooterIcon.vue"
export { default as EmbedFooterSeparator } from "./EmbedFooterSeparator.vue"
export { default as EmbedTimestamp } from "./EmbedTimestamp.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 { Embed } from "@/components/ui/embed"

export function Example() {
  return <Embed />
}
<script setup lang="ts">
import { Embed, EmbedAuthor, EmbedAuthorIcon, EmbedAuthorName, EmbedEyebrow, EmbedTitle, EmbedDescription, EmbedFields, EmbedField, EmbedFieldName, EmbedFieldValue, EmbedFooter, EmbedFooterIcon, EmbedFooterSeparator, EmbedTimestamp } from "@/components/ui/embed"
</script>

<template>
  <Embed />
</template>

API reference

Embed

PropType
color?string | undefined

Accent colour of the left bar. Any CSS colour; defaults to the brand.

Typestring | undefined

EmbedTimestamp

PropType
date?Date | number | string

The moment to render. Anything `new Date()` accepts.

TypeDate | number | string

The registry exports Embed, EmbedAuthor, EmbedAuthorIcon, EmbedAuthorName, EmbedEyebrow, EmbedTitle, EmbedDescription, EmbedFields, EmbedField, EmbedFieldName, EmbedFieldValue, EmbedFooter, EmbedFooterIcon, EmbedFooterSeparator, EmbedTimestamp. 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

Embed 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. In the plain HTML and CSS distribution the same component is newt-embed.