Welcome back! In the previous chapter, Isolated Animation Loop, we built a high-performance engine that ticks 20 times a second.
Currently, that engine is just spinning a small character (โ ). The actual text next to it (e.g., "generating code...") is static. It sits there, unmoving.
To make our AI feel truly "alive" and "intelligent," we want to send a wave of light through the text, similar to the "Slide to Unlock" animation on old iPhones or the "Skeleton Loaders" on modern websites.
Imagine you are in a long, dark tunnel. On the wall, the words "THINKING..." are painted. If you stand still, it looks flat. But if you walk past it holding a flashlight, the letters light up one by one as you pass, then fade back into the dark.
This "passing light" tells the user:
In Spinner, this is called the Glimmer Effect.
To achieve this, we need to master three concepts:
We use the component GlimmerMessage. We hook it up to the animation loop we built in Chapter 4.
The parent component calculates the glimmerIndex (which increases over time) and passes it down.
<GlimmerMessage
// The text to display
message="translating to python..."
// The spotlight position (calculated by our loop)
glimmerIndex={frame % 30}
// Base color (Darker)
messageColor="processing"
// Highlight color (Brighter)
shimmerColor="processingLight"
/>
What happens here?
How does the component know which parts to paint? It cuts the string into three pieces every time it renders.
Let's look at the code in GlimmerMessage.tsx.
Standard JavaScript strings split weirdly with emojis. We use Intl.Segmenter (a browser/Node standard) to split text by visual characters, not computer bytes.
// Inside GlimmerMessage.tsx
// We do this ONCE when the message changes, not every frame.
const { segments } = React.useMemo(() => {
const segs = [];
// This magic tool respects emojis!
for (const { segment } of getGraphemeSegmenter().segment(message)) {
segs.push({ segment, width: stringWidth(segment) });
}
return { segments: segs };
}, [message]);
Beginner Tip:
useMemois a React hook that caches the result. Splitting a string is expensive. We don't want to do it 20 times a second if the text hasn't changed!
Now that we have our safe list of characters (segments), we loop through them and sort them into our three buckets based on the glimmerIndex.
let before = '';
let shim = '';
let after = '';
let colPos = 0;
for (const { segment, width } of segments) {
// Is this character BEFORE the spotlight?
if (colPos + width <= glimmerIndex) {
before += segment;
}
// Is this character AFTER the spotlight?
else if (colPos > glimmerIndex + 1) {
after += segment;
}
// Then it must be UNDER the spotlight!
else {
shim += segment;
}
colPos += width;
}
Finally, we simply render the three strings side-by-side with different colors.
return (
<>
{/* Part 1: Darker */}
<Text color={messageColor}>{before}</Text>
{/* Part 2: Brighter (The Flashlight!) */}
<Text color={shimmerColor}>{shim}</Text>
{/* Part 3: Darker */}
<Text color={messageColor}>{after}</Text>
{/* Add a trailing space to prevent layout jumps */}
<Text color={messageColor}> </Text>
</>
);
Sometimes, the AI isn't just "thinking"; it is "using a tool" (like searching the web). When this happens, we want the text to flash entirely, pulsing like a heartbeat, rather than a wave passing through.
The GlimmerMessage component handles this by checking the mode prop.
if (mode === 'tool-use') {
// Interpolate between base color and shimmer color
// based on a pulsing 'flashOpacity' (0 to 1)
const interpolated = interpolateColor(
baseRGB,
shimmerRGB,
flashOpacity
);
return <Text color={toRGBColor(interpolated)}>{message}</Text>;
}
We have successfully added a professional polish to our UI.
But what happens if the "Thinking..." animation runs for 5 minutes? The user might think the AI has crashed. We need a way to detect if the agent has stopped responding and warn the user visually.
In the final chapter, we will build the safety system.
Next Chapter: Stall Detection (Heartbeat)
Generated by Code IQ