Chat Loader
Placeholders for a pending assistant reply, from typing dots to a message-shaped skeleton.
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
Anatomy
Usage
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.
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.
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.
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.
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.
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.
Accessibility
Every part is passive. Nothing is focusable and there are no keyboard interactions.
ARIA notes:
- Without
label,Dots,Pulse,SpinnerandSkeletonrenderaria-hidden="true"and are skipped by assistive tech. - With
label, the part becomes arole="status"live region and renders the text in a visually hidden<span>. There is noaria-labelas 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
Skeletonwrapping a labelledDotsannounces twice, and an unlabelledSkeletonhides any label inside it. - The inner
Spinneris alwaysaria-hidden; the wrapper'slabelis the only accessible text. ChatLoader,SkeletonAvatar,SkeletonBlockandSkeletonLineset no role. Setaria-busy="true"on the message list you are loading into, not on the loader.- Under
prefers-reduced-motion: reducethe 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:
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 anddata-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. TheSpinnerinside keeps its owndata-slot="spinner".data-slot="chat-loader-skeleton"on the skeleton row.data-slot="chat-loader-skeleton-avatar",data-slot="chat-loader-skeleton-block"anddata-slot="chat-loader-skeleton-line"on the skeleton parts.data-size="<size>"on the root,Dots,Pulse,Spinner,Skeleton,SkeletonAvatarandSkeletonLine. It is the resolved size, so a part inside<ChatLoader size="sm">reports"sm".
Related Components
- 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
Variants
ChatLoader.Dots
Three dots bouncing in sequence. Extends React.ComponentProps<"span"> without children.
Props
Variants
ChatLoader.Pulse
A single dot fading and scaling in and out. Extends React.ComponentProps<"span"> without children.
Props
Variants
ChatLoader.Spinner
Wraps the registry Spinner with variant="neutral-soft" so it lines up inside chat copy. Extends React.ComponentProps<"span"> without children.
Props
Variants
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
ChatLoader.SkeletonAvatar
Circular avatar placeholder with a soft pulse. Extends React.ComponentProps<"div"> without children.
Props
Variants
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
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
Variants
Types
The CVA recipes chatLoaderDotVariants, chatLoaderPulseVariants, chatLoaderSkeletonAvatarVariants and chatLoaderSkeletonLineVariants are exported for building matching placeholders outside the component.