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.
newt-embedimport {
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 embednpx newtui@latest add embedyarn dlx newtui@latest add embedbunx newtui@latest add embedCopy 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>•</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
color?string | undefined
Accent colour of the left bar. Any CSS colour; defaults to the brand.
string | undefinedEmbedTimestamp
date?Date | number | string
The moment to render. Anything `new Date()` accepts.
Date | number | stringThe 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.