Listen up. If you're building modern applications, understanding AI UX Polish is non-negotiable. This is where simple logic turns into intelligent behavior.
1Designing for AI's 'Thinking Time'
Even with streaming, there's always a 'Thinking Time' β a gap between the user submitting a request and the first token appearing, whether that's a model spinning up, a queued request, or the provider itself taking a beat before the stream starts. Designing the experience for these gaps is what makes an AI app feel premium.
Unlike a typical API call where response time is fairly predictable, AI response latency varies widely β a short prompt might return in under a second while a complex one takes several β so the loading experience needs to gracefully handle both the best case and the case where 'Thinking Time' stretches on longer than expected.
// Example
console.log("Running loading state UX...");AI logic processed successfully.
2Skeleton Screens vs. Spinners
Skeleton screens are better than spinners. They provide a blueprint of the content that's coming, reducing layout shift and user anxiety β a spinner tells the user 'something is happening' but gives no clue how much content to expect or where it will appear, while a skeleton reserves the actual space the real content will occupy.
The animate-pulse skeleton shown here matches the shape of the eventual AI response (a few lines of varying width, mimicking text), so when the real content swaps in, nothing jumps or reflows β the layout was already reserved by the skeleton's dimensions.
const AISkeleton = () => (
);AI logic processed successfully.
3'Thinking' Messages as Human-Centric Feedback
Progress indicators and 'Thinking' messages give the user a status update. Use them to show the AI is working, not just 'stuck' β an animated ellipsis paired with a status phrase like 'analyzing your request' communicates active progress in a way a static spinner alone can't.
For longer AI operations (multi-step agents, tool calls, or document processing), consider going further than a single message: cycling through specific status phrases tied to what's actually happening ('Reading document...', 'Searching for relevant sections...') gives the user real information instead of a vague, unchanging placeholder.
.
.
.
A.D.A is analyzing your request...
AI logic processed successfully.
4Optimistic UI for Instant Message Feedback
Optimistic UI: For quick actions, update the UI immediately before the server responds. For AI, this might mean adding the user's message to the chat instantly β the user's own message never depends on the AI, so there's no reason to wait for a round-trip before showing it.
This only applies to the part of the interaction that's guaranteed to succeed (the user's own message being added locally); the AI's reply still needs a real loading state, since unlike a simple form submission, you can't optimistically guess what the model is going to say.
const onSend = (msg) => {
setMessages([...messages, { role: 'user', content: msg }]);
// THEN call the API
fetchAI(msg);
};AI logic processed successfully.
5What You've Unlocked: Graceful Handling of AI Silence
UX polished! Your app now handles the 'silence' of AI computation with grace and style β skeleton screens, thinking indicators, and optimistic UI together turn the unavoidable gap between request and response from a source of user anxiety into a moment that feels intentional and controlled.
The common thread across all three patterns is the same: never leave the user staring at a static, unexplained wait. Whether that's a shaped placeholder, a status message, or an instantly-echoed action, the goal is always to communicate that progress is happening.
UX: Premium & Adaptive
AI logic processed successfully.
6What's Next: Machine Learning in the Browser
Next, we'll shift gears and look at the fascinating world of Machine Learning running directly in your browser β moving from designing around the latency of remote AI calls to a completely different model where inference happens locally, with no network round-trip at all.
The loading-state patterns covered here still matter even for client-side ML (a model still has to load and run inference), but the next lesson introduces an entirely new set of tradeoffs around privacy, performance, and hardware acceleration that don't apply to cloud-based AI calls.
ML in Browser Next
AI logic processed successfully.
7Step-by-Step Breakdown
Even with streaming, there's always a 'Thinking Time'. Designing the experience for these gaps is what makes an AI app feel premium.
Skeleton screens are better than spinners. They provide a blueprint of the content that's coming, reducing layout shift and user anxiety.
Progress indicators and 'Thinking' messages give the user a status update. Use them to show the AI is working, not just 'stuck'.
Checkpoint: What is 'Layout Shift' and why should we avoid it?
- βWhen elements 'jump' suddenly as content loads, making the UI feel unstable
- βWhen the background color changes
Optimistic UI: For quick actions, update the UI immediately before the server responds. For AI, this might mean adding the user's message to the chat instantly.
UX polished! Your app now handles the 'silence' of AI computation with grace and style.
Next, we'll shift gears and look at the fascinating world of Machine Learning running directly in your browser.
Run a Real Loading State Machine. Finish transitioning between loading states based on which event just occurred.
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Announce AI 'Thinking' State Changes to Screen Readers
A silently animating skeleton or dot-based thinking indicator conveys 'loading' visually but is invisible to a screen reader user unless the state change is explicitly announced. Wrap the loading indicator in an aria-live="polite" region so assistive tech announces when the AI starts and finishes responding.
<div role="status" aria-live="polite">{isThinking ? 'AI is generating a responseβ¦' : 'Response ready'}</div>SEO Implications
- 1
Skeleton Placeholder Text Should Never Be Mistaken for Real Content by Crawlers
Skeleton screens use empty divs with pulse animations rather than placeholder text, which is correct β but make sure no fallback 'Loading...' or lorem-ipsum-style text is left in the server-rendered HTML for an AI response, since a crawler that indexes the page before hydration could capture that placeholder instead of real content.
Best Practices
Size Skeleton Screens to Match Expected Content Dimensions
A skeleton that's much smaller or larger than the actual AI response causes the exact layout shift it's meant to prevent once real content swaps in. Base skeleton line counts and widths on the typical length of responses for that specific feature, not a generic one-size-fits-all placeholder.
Only Apply Optimistic UI to Actions That Can't Meaningfully Fail
Adding the user's own message to the chat instantly is safe because it doesn't depend on the AI. Don't extend optimistic UI to the AI's response itself β you can't optimistically render output you don't have yet, so that part still needs a real loading state and a defined error/retry path if the call fails.
Frequent Bugs
A skeleton screen's dimensions don't match the real AI response once it loads, causing a visible layout jump anyway β the exact problem skeletons are supposed to prevent.
Measure or estimate the typical response size for that specific feature and size the skeleton's lines/blocks to match closely, rather than reusing one generic skeleton component across every AI response type in the app.
Real-World Examples
A Multi-Stage Thinking Indicator for a Document-Analysis Agent
An AI document assistant shows a rotating set of status messages while it works through a multi-step task, so the user understands what's actually happening instead of staring at a static spinner for 8+ seconds.
const stages = ['Reading document...', 'Extracting key points...', 'Generating summary...'];
const [stage, setStage] = useState(0);
useEffect(() => {
if (!isThinking) return;
const interval = setInterval(() => setStage(s => (s + 1) % stages.length), 2000);
return () => clearInterval(interval);
}, [isThinking]);