v1.5

Chat Loader

Placeholders for a pending assistant reply, from typing dots to a message-shaped skeleton.

Pro
Can you summarize the onboarding feedback from last week?
Assistant is typing

Description

ChatLoader is a compound set of loading placeholders for AI chat. ChatLoader.Dots, ChatLoader.Pulse and ChatLoader.Spinner are inline <span> indicators. ChatLoader.Skeleton is a <div> shaped like an assistant message, built from avatar, block and line parts you can also arrange yourself. The ChatLoader root is only a layout row (avatar column plus content column) that hands its size down to every part inside it.

Use it in the gap between sending a prompt and the first streamed token: typing dots under the user's message, a skeleton while a long answer is generated, a spinner next to a tool step, or dots trailing streamed text while the model keeps going.

Don't use it for page or panel loading outside a conversation. For a generic busy state use Spinner, and for determinate work (uploads, imports) use Progress. If you want the status text itself to animate, use TextShimmer next to or instead of an indicator.

ChatLoader is a Create UI Pro component. Install it with a Pro seat.

Installation

pnpm dlx @create-ui/cli add chat-loader

Anatomy

<ChatLoader>
  <ChatLoader.SkeletonAvatar />
  <ChatLoader.SkeletonBlock>
    <ChatLoader.SkeletonLine />
    <ChatLoader.Dots />
  </ChatLoader.SkeletonBlock>
</ChatLoader>
 
<ChatLoader.Pulse />
<ChatLoader.Spinner />
<ChatLoader.Skeleton />

Usage

import { ChatLoader } from "@/components/ui/chat-loader"
<ChatLoader.Dots label="Assistant is typing" />

Examples

Every example is a Pro preview (marked with a Pro badge).

Indicators

Four loaders for four situations. Dots is the typing cue, Pulse is a quieter "still working" mark, Spinner suits tool calls and searches, and Skeleton holds the space of the message that is about to land.

Pro
DotsAssistant is typingPulseStill workingSpinnerLoading responseSkeleton
Loading message

Sizes

size takes "sm", "md" or "lg". Set it on the ChatLoader root and every nested part picks it up. A size on a part wins over the root, and a part with no size and no root renders md.

Pro

Skeleton

ChatLoader.Skeleton with no children renders an avatar and three lines, with the second and third line shortened so the block reads as a paragraph. The lines pulse gently, and the pulse is dropped under reduced motion.

Pro
Loading conversation

Custom skeleton

Pass children to Skeleton to match the message you expect: a user bubble with flex-row-reverse, or a two-paragraph answer with no avatar. Here one role="status" wrapper names the whole group, so the skeletons inside stay unlabelled.

Pro
Loading conversation history

Inline with text

Indicators are inline <span>s, so they sit inside a paragraph or next to a caption. Pair Spinner with TextShimmer for a tool step, or trail Dots after streamed text while the model is still writing.

Pro

Here is a first pass at the login form. I am now checking the validation rules

Searching the workspace
Still working on it

Loading sequence

A realistic hand-off: dots while the model reads the prompt, a skeleton while it writes, then the answer. The loaders are swapped, so they carry no label; a single stable role="status" line announces each phase and stops shimmering with active={false} once the answer is in.

Pro
Reading your question

Accessibility

Every part is passive. Nothing is focusable and there are no keyboard interactions.

KeyDescription
-Not focusable by default.

ARIA notes:

  • Without label, Dots, Pulse, Spinner and Skeleton render aria-hidden="true" and are skipped by assistive tech.
  • With label, the part becomes a role="status" live region and renders the text in a visually hidden <span>. There is no aria-label as well, so the message is read once.
  • Live regions announce changes, not their initial content. If you mount and unmount loaders as the reply progresses, leave them unlabelled and put one role="status" element in the page that stays mounted and changes its text (see Loading sequence).
  • Label one level only. A labelled Skeleton wrapping a labelled Dots announces twice, and an unlabelled Skeleton hides any label inside it.
  • The inner Spinner is always aria-hidden; the wrapper's label is the only accessible text.
  • ChatLoader, SkeletonAvatar, SkeletonBlock and SkeletonLine set no role. Set aria-busy="true" on the message list you are loading into, not on the loader.
  • Under prefers-reduced-motion: reduce the dots and pulse stop at 70% opacity, the skeleton stops pulsing, and the spinner holds a still arc.

Styling

Tailwind override: pass className to any part. Classes are merged with cn(), so a single line can take a new shape:

<ChatLoader.SkeletonLine className="rounded-component-xl h-8 w-40" />

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="chat-loader" on the root row.
  • data-slot="chat-loader-dots" on the dots wrapper and data-slot="chat-loader-dot" on each of its three dots.
  • data-slot="chat-loader-pulse" on the pulse.
  • data-slot="chat-loader-spinner" on the spinner wrapper. The Spinner inside keeps its own data-slot="spinner".
  • data-slot="chat-loader-skeleton" on the skeleton row.
  • data-slot="chat-loader-skeleton-avatar", data-slot="chat-loader-skeleton-block" and data-slot="chat-loader-skeleton-line" on the skeleton parts.
  • data-size="<size>" on the root, Dots, Pulse, Spinner, Skeleton, SkeletonAvatar and SkeletonLine. It is the resolved size, so a part inside <ChatLoader size="sm"> reports "sm".
[data-slot="chat-loader-dots"][data-size="lg"] [data-slot="chat-loader-dot"] {
  /* ... */
}
  • Spinner: a general loading indicator with color variants, for busy states outside a chat.
  • Text Shimmer: animates the status text itself ("Searching the workspace") instead of adding a mark.
  • Chat Message: the message the skeleton stands in for. Swap the loader out when the first token arrives.
  • Progress: use it when you know how far along the work is.

API Reference

ChatLoader

The layout row: an avatar column next to a content column. It renders no indicator of its own and provides size to every nested part. The function is also the namespace for the parts below, each of which is exported under its own name too (ChatLoaderDots, ChatLoaderSkeleton, and so on). Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
sizeChatLoaderSize"md"Scale handed down to every nested loader part.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Skeleton parts and indicators.

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"Provided through context; parts resolve their own class.

ChatLoader.Dots

Three dots bouncing in sequence. Extends React.ComponentProps<"span"> without children.

Props

PropTypeDefaultDescription
labelstring-Accessible message. Turns the dots into a role="status" live region.
sizeChatLoaderSizeinherited, then "md"Dot size. Overrides the enclosing loader's size.
classNamestring-Tailwind classes merged via cn().

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"Dots are 5px, 6px or 8px across.

ChatLoader.Pulse

A single dot fading and scaling in and out. Extends React.ComponentProps<"span"> without children.

Props

PropTypeDefaultDescription
labelstring-Accessible message. Turns the pulse into a role="status" live region.
sizeChatLoaderSizeinherited, then "md"Dot size. Overrides the enclosing loader's size.
classNamestring-Tailwind classes merged via cn().

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"The dot is 6px, 8px or 10px.

ChatLoader.Spinner

Wraps the registry Spinner with variant="neutral-soft" so it lines up inside chat copy. Extends React.ComponentProps<"span"> without children.

Props

PropTypeDefaultDescription
labelstring-Accessible message. Turns the wrapper into a role="status" live region.
sizeChatLoaderSizeinherited, then "md"Passed straight to Spinner size.
classNamestring-Tailwind classes merged onto the wrapper via cn().

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"The spinner is 16px, 20px or 24px.

ChatLoader.Skeleton

A chat-message-shaped placeholder row. With no children it renders an avatar and three lines. It provides its resolved size to the parts inside it. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
labelstring-Accessible message. Turns the skeleton into a role="status" live region.
sizeChatLoaderSizeinherited, then "md"Scale for the default parts and any parts passed as children.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNodeavatar plus 3 linesYour own arrangement of skeleton parts.

ChatLoader.SkeletonAvatar

Circular avatar placeholder with a soft pulse. Extends React.ComponentProps<"div"> without children.

Props

PropTypeDefaultDescription
sizeChatLoaderSizeinherited, then "md"Avatar size. Overrides the enclosing loader's size.
classNamestring-Tailwind classes merged via cn().

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"24px, 32px or 36px across.

ChatLoader.SkeletonBlock

Flex column that stacks lines next to the avatar and shrinks inside the row. Blocks can nest to make paragraphs. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Skeleton lines or indicators.

ChatLoader.SkeletonLine

One placeholder text line at full width. Inside a block, the second line is 5/6 wide and the third is 2/3. Extends React.ComponentProps<"div"> without children.

Props

PropTypeDefaultDescription
sizeChatLoaderSizeinherited, then "md"Line height. Overrides the enclosing loader's size.
classNamestring-Tailwind classes merged via cn().

Variants

VariantOptionsDefaultDescription
size"sm" "md" "lg""md"10px, 12px or 14px tall.

Types

type ChatLoaderSize = "sm" | "md" | "lg"

The CVA recipes chatLoaderDotVariants, chatLoaderPulseVariants, chatLoaderSkeletonAvatarVariants and chatLoaderSkeletonLineVariants are exported for building matching placeholders outside the component.