Message
A chat row: avatar, header, content and footer, top-aligned and mirrored with align.
import { Bubble, BubbleContent } from "@/components/ui/bubble";import { Button } from "@/components/ui/button";import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader,} from "@/components/ui/message";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <MessageGroup className="w-full max-w-lg gap-5"> <Message> <MessageAvatar className="size-6 border border-border bg-brand-soft text-brand-text"> <IconPlaceholder lucide="SparklesIcon" tabler="IconSparkles" hugeicons="SparklesIcon" phosphor="SparkleIcon" remixicon="RiSparklingLine" className="size-3" /> </MessageAvatar> <MessageContent> <MessageHeader> <span className="font-semibold text-foreground"> Research agent </span> <span className="font-mono">09:41</span> </MessageHeader> <Bubble variant="ghost"> <BubbleContent> I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage. </BubbleContent> </Bubble> <MessageFooter> <Button variant="ghost" size="icon-xs" aria-label="Copy"> <IconPlaceholder lucide="CopyIcon" tabler="IconCopy" hugeicons="CopyIcon" phosphor="CopyIcon" remixicon="RiFileCopyLine" /> </Button> <Button variant="ghost" size="icon-xs" aria-label="Like"> <IconPlaceholder lucide="ThumbsUpIcon" tabler="IconThumbUp" hugeicons="ThumbsUpIcon" phosphor="ThumbsUpIcon" remixicon="RiThumbUpLine" /> </Button> </MessageFooter> </MessageContent> </Message> <Message align="end"> <MessageAvatar className="size-6 bg-secondary font-mono text-[10px]"> DL </MessageAvatar> <MessageContent> <Bubble align="end"> <BubbleContent> Run it and send me the transaction to sign. </BubbleContent> </Bubble> <MessageFooter>Read</MessageFooter> </MessageContent> </Message> </MessageGroup> );}<script setup lang="ts">import { Bubble, BubbleContent } from "@/components/ui/bubble";import { Button } from "@/components/ui/button";import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader,} from "@/components/ui/message";import { CopyIcon, SparklesIcon, HeartIcon } from "@lucide/vue";</script>
<template> <MessageGroup class="w-full max-w-lg gap-5"> <Message> <MessageAvatar class="size-6 border border-border bg-brand-soft text-brand-text"> <SparklesIcon class="size-3" /> </MessageAvatar> <MessageContent> <MessageHeader> <span class="font-semibold text-foreground">Research agent</span> <span class="font-mono">09:41</span> </MessageHeader> <Bubble variant="ghost"> <BubbleContent> I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage. </BubbleContent> </Bubble> <MessageFooter> <Button variant="ghost" size="icon-xs" aria-label="Copy"><CopyIcon /></Button> <Button variant="ghost" size="icon-xs" aria-label="Like"><HeartIcon /></Button> </MessageFooter> </MessageContent> </Message> <Message align="end"> <MessageAvatar class="size-6 bg-secondary font-mono text-[10px]">DL</MessageAvatar> <MessageContent> <Bubble align="end"> <BubbleContent>Run it and send me the transaction to sign.</BubbleContent> </Bubble> <MessageFooter>Read</MessageFooter> </MessageContent> </Message> </MessageGroup></template><script lang="ts"> import * as Bubble from "$lib/components/ui/bubble"; import { Button } from "$lib/components/ui/button"; import * as Message from "$lib/components/ui/message";</script>
<Message.Group class="w-full max-w-lg gap-5"> <Message.Root> <Message.Avatar class="size-6 border border-border bg-brand-soft text-brand-text">✦</Message.Avatar> <Message.Content> <Message.Header> <span class="font-semibold text-foreground">Research agent</span> <span class="font-mono">09:41</span> </Message.Header> <Bubble.Root variant="ghost"> <Bubble.Content> I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage. </Bubble.Content> </Bubble.Root> <Message.Footer> <Button variant="ghost" size="icon-xs" aria-label="Copy">⧉</Button> <Button variant="ghost" size="icon-xs" aria-label="Like">👍</Button> </Message.Footer> </Message.Content> </Message.Root> <Message.Root align="end"> <Message.Avatar class="size-6 bg-secondary font-mono text-[10px]">DL</Message.Avatar> <Message.Content> <Bubble.Root align="end"> <Bubble.Content>Run it and send me the transaction to sign.</Bubble.Content> </Bubble.Root> <Message.Footer>Read</Message.Footer> </Message.Content> </Message.Root></Message.Group>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/messagepnpm dlx shadcn@latest add @edmi-ui/messageyarn dlx shadcn@latest add @edmi-ui/messagebunx shadcn@latest add @edmi-ui/messagePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/messagepnpm dlx shadcn-vue@latest add @edmi-ui/messageyarn dlx shadcn-vue@latest add @edmi-ui/messagebunx shadcn-vue@latest add @edmi-ui/messagePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-uiTerminal window pnpm add reka-uiTerminal window yarn add reka-uiTerminal window bun add reka-uiCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Bubble, BubbleContent } from "@/components/ui/bubble";import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup,} from "@/components/ui/message";
<MessageGroup className="w-full max-w-lg gap-5"> <Message> <MessageAvatar className="size-6 border border-border bg-brand-soft text-brand-text"> <SparkleIcon className="size-3" /> </MessageAvatar> {/* … */} </Message> <Message align="end"> <MessageAvatar className="size-6 bg-secondary font-mono text-[10px]"> DL </MessageAvatar> <MessageContent> <Bubble align="end"> <BubbleContent> Run it and send me the transaction to sign. </BubbleContent> </Bubble> <MessageFooter>Read</MessageFooter> </MessageContent> </Message></MessageGroup><script setup lang="ts">import { Bubble, BubbleContent } from "@/components/ui/bubble";import { Button } from "@/components/ui/button";import { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader,} from "@/components/ui/message";</script>
<template> <MessageGroup class="w-full max-w-lg gap-5"> <Message> <MessageAvatar class="size-6 border border-border bg-brand-soft text-brand-text"> <SparklesIcon class="size-3" /> </MessageAvatar> <MessageContent> <MessageHeader> <span class="font-semibold text-foreground">Research agent</span> <span class="font-mono">09:41</span> </MessageHeader> <Bubble variant="ghost"> <BubbleContent> I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage. </BubbleContent> </Bubble> <MessageFooter> <Button variant="ghost" size="icon-xs" aria-label="Copy"><CopyIcon /></Button> <Button variant="ghost" size="icon-xs" aria-label="Like"><HeartIcon /></Button> </MessageFooter> </MessageContent> </Message> <!-- … --> </MessageGroup></template><script lang="ts"> import * as Bubble from "$lib/components/ui/bubble"; import * as Message from "$lib/components/ui/message"; import { Button } from "$lib/components/ui/button";</script>
<Message.Group class="w-full max-w-lg gap-5"> <Message.Root> <Message.Avatar class="size-6 border border-border bg-brand-soft text-brand-text">✦</Message.Avatar> <Message.Content> <Message.Header> <span class="font-semibold text-foreground">Research agent</span> <span class="font-mono">09:41</span> </Message.Header> <Bubble.Root variant="ghost"> <Bubble.Content> I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage. </Bubble.Content> </Bubble.Root> <Message.Footer> <Button variant="ghost" size="icon-xs" aria-label="Copy">⧉</Button> <Button variant="ghost" size="icon-xs" aria-label="Like">👍</Button> </Message.Footer> </Message.Content> </Message.Root> <!-- … --></Message.Group>Component source
Section titled “Component source”import { cn } from "cn";import type * as React from "react";
function MessageGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="message-group" className={cn("flex min-w-0 flex-col gap-2", className)} {...props} /> );}
function Message({ className, align = "start", ...props}: React.ComponentProps<"div"> & { align?: "start" | "end" }) { return ( <div data-slot="message" data-align={align} className={cn( "group/message relative flex w-full min-w-0 items-start gap-2.5 text-sm data-[align=end]:flex-row-reverse", className, )} {...props} /> );}
// Avatar is top-aligned with the header line, or with the first bubble line when there is no header (DESIGN §4.8).function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="message-avatar" className={cn( "mt-[5px] flex w-fit min-w-6 shrink-0 items-center justify-center self-start overflow-hidden rounded-full group-has-data-[slot=message-header]/message:mt-[3px]", className, )} {...props} /> );}
function MessageContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="message-content" className={cn( "flex w-full min-w-0 flex-col gap-1.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end", className, )} {...props} /> );}
function MessageHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="message-header" className={cn( "flex h-[30px] max-w-full min-w-0 items-center gap-2 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0", className, )} {...props} /> );}
function MessageFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="message-footer" className={cn( "mt-0.5 flex max-w-full min-w-0 items-center gap-1 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end", className, )} {...props} /> );}
export { Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader,};<script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"] align?: "start" | "end"}const props = withDefaults(defineProps<Props>(), { align: "start", as: "div",})</script>
<template> <Primitive data-slot="message" :data-align="align" :as="as" :as-child="asChild" :class="cn( 'group/message relative flex w-full min-w-0 items-start gap-2.5 text-sm data-[align=end]:flex-row-reverse', props.class, )" > <slot /> </Primitive></template><script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"]}const props = withDefaults(defineProps<Props>(), { as: "div",})</script>
<template> <Primitive data-slot="message-avatar" :as="as" :as-child="asChild" :class="cn( 'mt-[5px] flex w-fit min-w-6 shrink-0 items-center justify-center self-start overflow-hidden rounded-full group-has-data-[slot=message-header]/message:mt-[3px]', props.class, )" > <slot /> </Primitive></template><script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"]}const props = withDefaults(defineProps<Props>(), { as: "div",})</script>
<template> <Primitive data-slot="message-content" :as="as" :as-child="asChild" :class="cn( 'flex w-full min-w-0 flex-col gap-1.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end', props.class, )" > <slot /> </Primitive></template><script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"]}const props = withDefaults(defineProps<Props>(), { as: "div",})</script>
<template> <Primitive data-slot="message-footer" :as="as" :as-child="asChild" :class="cn( 'mt-0.5 flex max-w-full min-w-0 items-center gap-1 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end', props.class, )" > <slot /> </Primitive></template><script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"]}const props = withDefaults(defineProps<Props>(), { as: "div",})</script>
<template> <Primitive data-slot="message-group" :as="as" :as-child="asChild" :class="cn( 'flex min-w-0 flex-col gap-2', props.class, )" > <slot /> </Primitive></template><script lang='ts' setup>import type { PrimitiveProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { Primitive } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
interface Props extends PrimitiveProps { class?: HTMLAttributes["class"]}const props = withDefaults(defineProps<Props>(), { as: "div",})</script>
<template> <Primitive data-slot="message-header" :as="as" :as-child="asChild" :class="cn( 'flex h-[30px] max-w-full min-w-0 items-center gap-2 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0', props.class, )" > <slot /> </Primitive></template>export { default as Message } from "./Message.vue";export { default as MessageAvatar } from "./MessageAvatar.vue";export { default as MessageContent } from "./MessageContent.vue";export { default as MessageFooter } from "./MessageFooter.vue";export { default as MessageGroup } from "./MessageGroup.vue";export { default as MessageHeader } from "./MessageHeader.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="message-avatar" class={cn( "mt-[5px] flex w-fit min-w-6 shrink-0 items-center justify-center self-start overflow-hidden rounded-full group-has-data-[slot=message-header]/message:mt-[3px]", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="message-content" class={cn( "flex w-full min-w-0 flex-col gap-1.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="message-footer" class={cn( "mt-0.5 flex max-w-full min-w-0 items-center gap-1 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="message-group" class={cn( "flex min-w-0 flex-col gap-2", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="message-header" class={cn( "flex h-[30px] max-w-full min-w-0 items-center gap-2 px-[13px] text-xs text-muted-foreground group-has-data-[variant=ghost]/message:px-0", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, align = "start", children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { align?: "start" | "end" } = $props();</script>
<div bind:this={ref} data-slot="message" data-align={align} class={cn( "group/message relative flex w-full min-w-0 items-start gap-2.5 text-sm data-[align=end]:flex-row-reverse", className )} {...restProps}> {@render children?.()}</div>import Root from "./message.svelte";import Avatar from "./message-avatar.svelte";import Content from "./message-content.svelte";import Footer from "./message-footer.svelte";import Group from "./message-group.svelte";import Header from "./message-header.svelte";
export { Avatar, Avatar as MessageAvatar, Content, Content as MessageContent, Footer, Footer as MessageFooter, Group, Group as MessageGroup, Header, Header as MessageHeader, Root, // Root as Message,};| Part | Prop | Type | Default |
|---|---|---|---|
| Message | align | start | end |
start |
| MessageGroup, MessageAvatar, MessageContent, MessageHeader, MessageFooter | div props |
✦ Edmi additions
Section titled “✦ Edmi additions”- The avatar is top-aligned with the header line, or with the first bubble line when there is no header.
- The header is a fixed 30px row; the footer follows the side of the message.