Reply Preview
A compact "replying to" bar showing the author, a truncated excerpt, or the slash command that was used.
A reply preview keeps the referenced author and excerpt attached to the message that answers it. Compose the avatar, author, text, action and command parts so the compact row still reflects the content being referenced.
newt-replyimport {
ReplyPreview,
ReplyPreviewAction,
ReplyPreviewAuthor,
ReplyPreviewAvatar,
ReplyPreviewCommand,
ReplyPreviewText,
} from "@/registry/bases/newt/ui/reply-preview"
/* Inline so the demo never reaches out to the network for a placeholder. */
const AVATAR =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8' fill='%235865f2'/%3E%3C/svg%3E"
export default function ReplyPreviewDemo() {
return (
<div className="flex flex-col gap-3">
<ReplyPreview>
<ReplyPreviewAvatar src={AVATAR} />
<ReplyPreviewAuthor>@username</ReplyPreviewAuthor>
<ReplyPreviewText>
original message text being replied to
</ReplyPreviewText>
</ReplyPreview>
<ReplyPreview>
<ReplyPreviewAvatar src={AVATAR} />
<ReplyPreviewAuthor>@username</ReplyPreviewAuthor>
<ReplyPreviewAction>used</ReplyPreviewAction>
<ReplyPreviewCommand>/ban member</ReplyPreviewCommand>
</ReplyPreview>
</div>
)
}<script setup lang="ts">
import {
ReplyPreview,
ReplyPreviewAction,
ReplyPreviewAuthor,
ReplyPreviewAvatar,
ReplyPreviewCommand,
ReplyPreviewText,
} from "~~/registry/bases/newt/ui/reply-preview"
/* Inline so the demo never reaches out to the network for a placeholder. */
const AVATAR =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8' fill='%235865f2'/%3E%3C/svg%3E"
</script>
<template>
<div class="flex flex-col gap-3">
<ReplyPreview>
<ReplyPreviewAvatar :src="AVATAR" />
<ReplyPreviewAuthor>@username</ReplyPreviewAuthor>
<ReplyPreviewText
>original message text being replied to</ReplyPreviewText
>
</ReplyPreview>
<ReplyPreview>
<ReplyPreviewAvatar :src="AVATAR" />
<ReplyPreviewAuthor>@username</ReplyPreviewAuthor>
<ReplyPreviewAction>used</ReplyPreviewAction>
<ReplyPreviewCommand>/ban member</ReplyPreviewCommand>
</ReplyPreview>
</div>
</template>Installation
pnpm dlx newtui@latest add reply-previewnpx newtui@latest add reply-previewyarn dlx newtui@latest add reply-previewbunx newtui@latest add reply-previewCopy the source into your project
import * as React from "react"
import { cn } from "@/lib/utils"
const ReplyPreview = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"relative mb-1 flex min-w-0 items-center gap-1.5 ps-[18px] text-[13px] leading-4 text-newt-text-muted",
/* The elbow that hooks the preview back up to the message above it. */
"before:absolute before:start-0 before:top-1/2 before:h-3 before:w-3 before:-translate-y-full before:rounded-ss-[6px] before:border-s-2 before:border-t-2 before:border-newt-border before:content-['']",
className
)}
{...props}
/>
))
ReplyPreview.displayName = "ReplyPreview"
const ReplyPreviewAvatar = 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}
/>
))
ReplyPreviewAvatar.displayName = "ReplyPreviewAvatar"
const ReplyPreviewAuthor = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn("shrink-0 font-medium text-newt-text-link", className)}
{...props}
/>
))
ReplyPreviewAuthor.displayName = "ReplyPreviewAuthor"
const ReplyPreviewText = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
"min-w-0 max-w-[360px] truncate text-newt-text-muted",
className
)}
{...props}
/>
))
ReplyPreviewText.displayName = "ReplyPreviewText"
/* "used" — the verb between the author and an interaction chip. */
const ReplyPreviewAction = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
<span ref={ref} className={cn("shrink-0", className)} {...props} />
))
ReplyPreviewAction.displayName = "ReplyPreviewAction"
/* The command chip shown when the reply is a slash-command interaction. */
const ReplyPreviewCommand = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<"span">
>(({ className, children, ...props }, ref) => (
<span
ref={ref}
className={cn(
"inline-flex min-w-0 items-center gap-0.5 rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_15%,transparent)] px-1 font-medium text-newt-mention-text",
className
)}
{...props}
>
<span className="truncate">{children}</span>
</span>
))
ReplyPreviewCommand.displayName = "ReplyPreviewCommand"
export {
ReplyPreview,
ReplyPreviewAvatar,
ReplyPreviewAuthor,
ReplyPreviewText,
ReplyPreviewAction,
ReplyPreviewCommand,
}<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(
'relative mb-1 flex min-w-0 items-center gap-1.5 ps-[18px] text-[13px] leading-4 text-newt-text-muted',
// The elbow that hooks the preview back up to the message above it.
`before:absolute before:start-0 before:top-1/2 before:h-3 before:w-3 before:-translate-y-full before:rounded-ss-[6px] before:border-s-2 before:border-t-2 before:border-newt-border before:content-['']`,
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>
<template>
<span :class="cn('shrink-0 font-medium text-newt-text-link', 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('min-w-0 max-w-[360px] truncate text-newt-text-muted', 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>
<!-- "used" — the verb between the author and an interaction chip. -->
<template>
<span :class="cn('shrink-0', 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>
<!-- The command chip shown when the reply is a slash-command interaction. -->
<template>
<span
:class="
cn(
'inline-flex min-w-0 items-center gap-0.5 rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_15%,transparent)] px-1 font-medium text-newt-mention-text',
props.class
)
"
>
<span class="truncate"><slot /></span>
</span>
</template>export { default as ReplyPreview } from "./ReplyPreview.vue"
export { default as ReplyPreviewAvatar } from "./ReplyPreviewAvatar.vue"
export { default as ReplyPreviewAuthor } from "./ReplyPreviewAuthor.vue"
export { default as ReplyPreviewText } from "./ReplyPreviewText.vue"
export { default as ReplyPreviewAction } from "./ReplyPreviewAction.vue"
export { default as ReplyPreviewCommand } from "./ReplyPreviewCommand.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 { ReplyPreview } from "@/components/ui/reply-preview"
export function Example() {
return <ReplyPreview />
}<script setup lang="ts">
import { ReplyPreview, ReplyPreviewAvatar, ReplyPreviewAuthor, ReplyPreviewText, ReplyPreviewAction, ReplyPreviewCommand } from "@/components/ui/reply-preview"
</script>
<template>
<ReplyPreview />
</template>API reference
ReplyPreview
children / default slot?React.ReactNode / Vue slot
The avatar, author and referenced text or command, in reading order.
React.ReactNode / Vue slotclassName / class?string
Additional classes merged with the preview defaults.
stringThe registry also exports ReplyPreviewAvatar, ReplyPreviewAuthor, ReplyPreviewText, ReplyPreviewAction and ReplyPreviewCommand. Each part accepts the native attributes of the element it renders.
Accessibility
The avatar is decorative by default; provide alternative text only when it adds information that the adjacent author name does not. Keep the referenced text in DOM order, and use a real button or link outside the preview when the row itself opens the original message.
Tokens
Reply Preview 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-reply.