ChatMessage
Overview
The ChatMessage is a chat-bubble container used to render a single turn in an AI chat transcript. It is composed of four recipe parts: useChatMessageRecipe() for the side-aware root wrapper, useChatMessageAvatarRecipe() for the leading avatar slot, useChatMessageContentRecipe() for the bubble itself, and useChatMessageActionsRecipe() for the hover-revealed action row beneath the bubble. Each composable creates a fully configured recipe with color, variant, and size options — plus compound variants on the content recipe that handle every color-variant combination automatically.
The ChatMessage recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
Why use the ChatMessage recipe?
The ChatMessage recipe helps you:
- Ship faster with sensible defaults: Get 3 colors, 5 visual styles, 3 sizes, and start/end side alignment out of the box with a single set of composable calls.
- Compose flexible chat layouts: Four coordinated recipes (root, avatar, content, actions) share the same variant axes, so messages stay internally consistent as you mix user and assistant turns.
- Stay i18n-friendly: The
sideaxis uses logicalstartandendvalues that flip automatically underdir="rtl", so right-to-left layouts work without extra code. - Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, size, or side values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the ChatMessage recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import {
useChatMessageRecipe,
useChatMessageAvatarRecipe,
useChatMessageContentRecipe,
useChatMessageActionsRecipe,
} from '@styleframe/theme';
const s = styleframe();
const chatMessage = useChatMessageRecipe(s);
const chatMessageAvatar = useChatMessageAvatarRecipe(s);
const chatMessageContent = useChatMessageContentRecipe(s);
const chatMessageActions = useChatMessageActionsRecipe(s);
export default s;
Build the component
Import the chatMessage, chatMessageAvatar, chatMessageContent, and chatMessageActions runtime functions from the virtual module and pass variant props to compute class names:
import {
chatMessage,
chatMessageAvatar,
chatMessageContent,
chatMessageActions,
} from "virtual:styleframe";
interface ChatMessageProps {
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle" | "ghost";
size?: "sm" | "md" | "lg";
side?: "start" | "end";
avatar?: React.ReactNode;
actions?: React.ReactNode;
children?: React.ReactNode;
}
export function ChatMessage({
color = "neutral",
variant = "subtle",
size = "md",
side = "start",
avatar,
actions,
children,
}: ChatMessageProps) {
return (
<article className={chatMessage({ color, variant, size, side })}>
{avatar && (
<div className={chatMessageAvatar({ color, variant, size })}>
{avatar}
</div>
)}
<div>
<div className={chatMessageContent({ color, variant, size })}>
{children}
</div>
{actions && (
<div className={chatMessageActions({ color, variant, size })}>
{actions}
</div>
)}
</div>
</article>
);
}
<script setup lang="ts">
import {
chatMessage,
chatMessageAvatar,
chatMessageContent,
chatMessageActions,
} from "virtual:styleframe";
const {
color = "neutral",
variant = "subtle",
size = "md",
side = "start",
} = defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle" | "ghost";
size?: "sm" | "md" | "lg";
side?: "start" | "end";
}>();
</script>
<template>
<article :class="chatMessage({ color, variant, size, side })">
<div :class="chatMessageAvatar({ color, variant, size })">
<slot name="avatar" />
</div>
<div>
<div :class="chatMessageContent({ color, variant, size })">
<slot />
</div>
<div :class="chatMessageActions({ color, variant, size })">
<slot name="actions" />
</div>
</div>
</article>
</template>
See it in action
Colors
The ChatMessage recipe includes 3 color variants: light, dark, and neutral. Like the Card recipe, ChatMessage uses neutral-spectrum colors designed for content surfaces rather than status communication — chat sides aren't semantic states, they're conversational roles. The content recipe combines each color with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides.
The neutral color adapts automatically: it uses a light appearance in light mode and a dark appearance in dark mode, making it the safest default for general-purpose chat UIs.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.white / @color.gray-* | Light surfaces, stays light in dark mode |
dark | @color.gray-900 | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default chat-message color. It adapts to the user's color scheme automatically, so you don't need to manage light and dark variants separately.Variants
Five visual style variants control how the message bubble is rendered. Each variant is combined with the selected color through compound variants on the content recipe, so you always get the correct background, text, and border colors for your chosen color.
Solid
Filled background with a subtle border. The most prominent style, ideal for emphasizing a single message in a transcript.
Outline
Transparent background with a colored border and matching text. A lightweight, definitionful style that works well for system or meta messages.
Soft
Light tinted background with no visible border. A gentle, borderless style that reads like a typical user chat bubble.
Subtle
Light tinted background with a matching border. Combines the softness of the soft variant with added visual definition from a border — the recommended default for both user and assistant turns.
Ghost
No background, no border, no padding. Strips the bubble entirely so the message reads as inline prose. Use for assistant turns that should feel like part of the page rather than a discrete bubble.
Sizes
Three size variants from sm to lg control the avatar dimensions, the bubble padding and border radius, and the gap between the avatar and the bubble.
Size Reference
| Size | Avatar | Bubble Border Radius | Bubble Padding (V / H) | Root Gap | Actions Gap / Margin |
|---|---|---|---|---|---|
sm | @1.5 | @border-radius.sm | @0.5 / @0.75 | @0.5 | @0.125 |
md | @2 | @border-radius.md | @0.75 / @1 | @0.75 | @0.25 |
lg | @2.5 | @border-radius.lg | @1 / @1.25 | @1 | @0.375 |
size prop must be passed to each sub-recipe individually so the avatar, bubble, and actions row stay visually balanced. The root recipe controls the gap; the sub-recipes manage their own dimensions and padding.Side
The side axis on the root recipe controls how the message aligns within its container. Logical values mean both work correctly under dir="rtl".
| Side | Behavior | Use Case |
|---|---|---|
start | justifyContent: flex-start, default flex direction | Assistant or system messages |
end | justifyContent: flex-end, flexDirection: row-reverse | User messages — avatar appears on the inline-end |
side based on the message author, not on a fixed left/right position. Logical values keep your UI correct in right-to-left languages without changes.Anatomy
The ChatMessage recipe is composed of four independent recipes that work together to form the bubble:
| Part | Recipe | Role |
|---|---|---|
| Root | useChatMessageRecipe() | Outer <article> flex container; owns the side axis for alignment |
| Avatar | useChatMessageAvatarRecipe() | Leading slot for an avatar or icon — rounded, shrink-0, with a neutral placeholder background |
| Content | useChatMessageContentRecipe() | The bubble itself — background, padding, border, and the color × variant compound matrix |
| Actions | useChatMessageActionsRecipe() | Action button row beneath the bubble — flex layout with size-aware gap and margin |
The chat-message-avatar recipe is independent of the media recipe: drop a raw <img> or icon inside an element styled by chatMessageAvatar() and you get correct sizing and shape without pulling in any other recipe. Pass color, variant, and size consistently to all four sub-recipes so they stay visually balanced.
<!-- All four parts working together -->
<article class="chatMessage(...)">
<div class="chatMessageAvatar(...)">A</div>
<div>
<div class="chatMessageContent(...)">Message body</div>
<div class="chatMessageActions(...)">Action buttons</div>
</div>
</article>
useChatMessageRecipe() wrapping useChatMessageContentRecipe(). Add the avatar and actions only when your design needs them.Accessibility
- Use semantic markup. The root renders as an
<article>element, which communicates a self-contained message to assistive technology. Don't replace it with a<div>unless you have a strong reason. - Announce streaming updates. If your chat UI streams assistant responses, wrap the message list in a region with
aria-live="polite"so screen readers hear new content without interrupting the user. - Provide accessible names for action buttons. Buttons in the actions row should have visible text or an
aria-label("Copy message","Regenerate response") so users on assistive tech know what each action does. - Verify contrast ratios. The
solidvariant withdarkcolor places light text on a dark background. Default tokens meet WCAG AA 4.5:1 contrast. If you override colors, verify with the WebAIM Contrast Checker. - Don't rely on side alone to convey author. Pair
side="end"with author text or an avatar so the role is communicated to users who don't perceive the layout (screen reader, low-vision, RTL contexts).
Customization
Overriding Defaults
Each chat-message composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import {
useChatMessageRecipe,
useChatMessageContentRecipe,
} from '@styleframe/theme';
const s = styleframe();
const chatMessage = useChatMessageRecipe(s, {
base: { gap: '@1' },
defaultVariants: {
color: 'neutral',
variant: 'soft',
size: 'lg',
side: 'start',
},
});
const chatMessageContent = useChatMessageContentRecipe(s, {
base: { maxWidth: '60%' },
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useChatMessageContentRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate neutral color with the bubble variants you actually use
const chatMessageContent = useChatMessageContentRecipe(s, {
filter: {
color: ['neutral'],
variant: ['subtle', 'ghost'],
},
});
export default s;
API Reference
useChatMessageRecipe(s, options?)
Creates the chat-message root recipe — a flex container with logical side alignment. Owns the side axis; the other axes are exposed for prop spreading but produce no styles at the root.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the root container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle, ghost | subtle |
size | sm, md, lg | md |
side | start, end | start |
useChatMessageAvatarRecipe(s, options?)
Creates the chat-message avatar recipe — a fixed-size leading slot with a circular placeholder. Independent of the media recipe. Accepts the same parameters as useChatMessageRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle, ghost | subtle |
size | sm, md, lg | md |
useChatMessageContentRecipe(s, options?)
Creates the chat-message content recipe — the bubble itself. Owns the 12-entry color × variant compound matrix (3 colors × 4 visual variants). The ghost variant strips bubble chrome regardless of color, so it has no compound entries. Accepts the same parameters as useChatMessageRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle, ghost | subtle |
size | sm, md, lg | md |
useChatMessageActionsRecipe(s, options?)
Creates the chat-message actions row recipe — a flex layout for action buttons displayed beneath the bubble. Hover-reveal is a markup concern (toggle opacity on the parent), not a recipe variant. Accepts the same parameters as useChatMessageRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle, ghost | subtle |
size | sm, md, lg | md |
Best Practices
- Pass
color,variant, andsizeconsistently: All four sub-recipes accept the same axes. Spread the same props to each so the avatar, bubble, and actions row stay visually balanced. - Choose
sideby author, not by position: Useside="start"for assistant turns andside="end"for user turns. Logical values keep your UI correct in right-to-left languages. - Use
ghostfor assistant prose,subtlefor user replies: Ghost reads like part of the page; subtle reads like a discrete message. The contrast helps users distinguish authorship at a glance. - Filter what you don't need: If your chat UI uses only
subtleandghost, pass afilteroption to remove the other variants from the generated CSS. - Override defaults at the recipe level: Set your most common variant combination as
defaultVariantsso component consumers write less code. - Don't put hover reveal on the recipe: Toggle action opacity in your component template (
group-hoverin CSS,:hoverin your wrapper). Keep the recipe declarative.
FAQ
useChatMessageRecipe() wrapping useChatMessageContentRecipe() — just an aligned bubble with no avatar or actions row. Add the avatar and actions recipes only when your design needs them.light, dark, and neutral to provide surface variations that work across both user and assistant turns without implying status.subtle renders a tinted background with a matching border — a discrete bubble. ghost strips background, border, and padding entirely so the message reads as inline prose. Use subtle when the message should feel like a chat bubble; use ghost when an assistant response should feel like part of the surrounding page (typical for long-form generated content).dir="rtl". If your app supports right-to-left languages (Arabic, Hebrew, Persian), side="end" keeps user messages on the user's natural side without you writing any directional code. If you never plan to support RTL, the values still read clearly: start is where the conversation begins (typically the assistant), end is where the user replies.Compound variants live on the content recipe (useChatMessageContentRecipe). They map each color-variant combination to specific styles. For example, when color is neutral and variant is subtle, the compound variant applies background: @color.gray-100, color: @color.gray-700, and borderColor: @color.gray-200, along with dark mode overrides. The recipe has 12 compound entries (3 colors × 4 visual variants — the ghost variant strips chrome regardless of color, so it needs no compound entries).
filter option on the content recipe, compound variants that reference filtered-out values are automatically removed. For example, if you filter variant to only ['subtle', 'ghost'], all compound variants matching solid, outline, or soft are excluded from the generated output. Default variants are also adjusted if they reference a removed value..chat-message:hover .chat-message-actions { opacity: 1 }. Keeping reveal logic out of the recipe lets you choose between hover, focus-within, always-visible, or other patterns without changing the recipe.Placeholder
A visual placeholder container with a dashed border and hatch pattern for layout prototyping, wireframing, and empty states. Uses the recipe system with dark mode support.
Overview
Explore Styleframe's comprehensive design token system. Create consistent, scalable design systems with composable functions for colors, typography, spacing, and more.