Styleframe Logo
AI Chat

ChatMessage

A chat-bubble component for AI chat transcripts with optional leading avatar, content bubble, and hover-revealed actions row. Side-aware layout with logical start/end alignment, plus compound variants that handle every color-variant combination.

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 side axis uses logical start and end values that flip automatically under dir="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:

src/components/chat-message.styleframe.ts
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:

src/components/ChatMessage.tsx
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>
    );
}

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

ColorTokenUse Case
light@color.white / @color.gray-*Light surfaces, stays light in dark mode
dark@color.gray-900Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use 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

SizeAvatarBubble Border RadiusBubble Padding (V / H)Root GapActions 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
Good to know: The 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".

SideBehaviorUse Case
startjustifyContent: flex-start, default flex directionAssistant or system messages
endjustifyContent: flex-end, flexDirection: row-reverseUser messages — avatar appears on the inline-end
Pro tip: Set 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:

PartRecipeRole
RootuseChatMessageRecipe()Outer <article> flex container; owns the side axis for alignment
AvataruseChatMessageAvatarRecipe()Leading slot for an avatar or icon — rounded, shrink-0, with a neutral placeholder background
ContentuseChatMessageContentRecipe()The bubble itself — background, padding, border, and the color × variant compound matrix
ActionsuseChatMessageActionsRecipe()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>
Pro tip: You don't have to use all four parts. A minimal chat-message is just 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 solid variant with dark color 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:

src/components/chat-message.styleframe.ts
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:

src/components/chat-message.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

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:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the root container
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtle, ghostsubtle
sizesm, md, lgmd
sidestart, endstart

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:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtle, ghostsubtle
sizesm, md, lgmd

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:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtle, ghostsubtle
sizesm, md, lgmd

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:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtle, ghostsubtle
sizesm, md, lgmd

Learn more about recipes →

Best Practices

  • Pass color, variant, and size consistently: 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 side by author, not by position: Use side="start" for assistant turns and side="end" for user turns. Logical values keep your UI correct in right-to-left languages.
  • Use ghost for assistant prose, subtle for 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 subtle and ghost, pass a filter option to remove the other variants from the generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.
  • Don't put hover reveal on the recipe: Toggle action opacity in your component template (group-hover in CSS, :hover in your wrapper). Keep the recipe declarative.

FAQ