Skip to content
edmi-ui

Message

A chat row: avatar, header, content and footer, top-aligned and mirrored with align.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Research agent09:41
I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage.
DL
Run it and send me the transaction to sign.
Read
Research agent09:41
I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage.
DL
Run it and send me the transaction to sign.
Read
✦
Research agent 09:41
I screened the megacaps against your mandate. A rebalance would cost about 0.08% in slippage.
DL
Run it and send me the transaction to sign.
Read
Terminal window
npx shadcn@latest add @edmi-ui/message
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/message
Terminal window
yarn dlx shadcn@latest add @edmi-ui/message
Terminal window
bunx shadcn@latest add @edmi-ui/message

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/message
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/message
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/message
Terminal window
bunx shadcn-vue@latest add @edmi-ui/message

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/message.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/message.tsx
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,
};
registry/ui/message/Message.vue
<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>
registry/ui/message/MessageAvatar.vue
<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>
registry/ui/message/MessageContent.vue
<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>
registry/ui/message/MessageFooter.vue
<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>
registry/ui/message/MessageGroup.vue
<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>
registry/ui/message/MessageHeader.vue
<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>
registry/ui/message/index.ts
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";
src/lib/registry/ui/message/message-avatar.svelte
<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>
src/lib/registry/ui/message/message-content.svelte
<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>
src/lib/registry/ui/message/message-footer.svelte
<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>
src/lib/registry/ui/message/message-group.svelte
<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>
src/lib/registry/ui/message/message-header.svelte
<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>
src/lib/registry/ui/message/message.svelte
<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>
src/lib/registry/ui/message/index.ts
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
  • 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.