Global Perspectives Global Perspectives

Engineering

Building Cinematic Loading Screens: How We Explored Pretext and What We Built Instead

April 2, 2026

Loading screens are usually an afterthought — a spinner, a skeleton card, a placeholder. We wanted ours to do something more: use the actual intelligence data the app is fetching to show a brief, animated preview of the world before the page finishes loading. It should feel like the app is thinking, not stalling.

That goal led us to explore Pretext, a text layout library by Cheng Lou. Here is what we found, why we ultimately did not use it, and what we built instead.

What is Pretext?

Pretext is a low-level text layout engine for the web. The key idea is that it measures and flows text entirely in JavaScript, without touching the DOM. That sounds like an unusual constraint, but it unlocks something valuable: you can compute the exact position of every character before anything is rendered, and then render only what you need — to a canvas, to an SVG, or to absolutely-positioned DOM nodes — at 60fps.

The core API revolves around a few primitives:

The obstacle-aware layout is genuinely novel. Most CSS text-flow-around-shapes approaches rely on shape-outside, which only works in float contexts and has no animation story. Pretext gives you the positions mathematically, so you can animate obstacles and reflow text on every frame.

The use case we imagined: as our country constellation animation renders — circular node badges for each country, connected by edges — text headlines could flow around the nodes in real time. The whole loading screen would be a live, typographic map.

Where It Got Complicated

The vision worked on paper. In practice, a few things gave us pause.

Font loading timing. Pretext needs to measure glyphs before layout, which means the fonts must be fully loaded before the first frame. Our loading screen is shown precisely when the app is in its most fragile state — fonts may still be in flight, data may not be cached yet. Adding a font-readiness gate to an already-loading screen felt like the wrong tradeoff.

Obstacle coordinates require DOM measurements. To flow text around the country node badges, we need to know where those nodes are on screen. But those positions depend on the container dimensions, which are only available after the first render. We would have needed a two-pass approach: render the nodes, measure their positions, feed those into Pretext, then re-render the text. That added complexity for a loading screen that will be visible for only a few seconds.

The library is pre-release. Pretext is an experimental project. The API is thoughtful and the performance story is compelling, but shipping a pre-release dependency in a production loading path — where reliability is critical — felt premature.

What We Built Instead

We stepped back and asked: what is the loading screen actually supposed to communicate? Not "text is flowing around circles." The real goal is: here is the intelligence you are about to see — countries, stories, connections. That insight led to a different design.

We built two animation modes, both using plain React state and CSS:

Typewriter → Constellation. A sentence types out describing the day's top headlines and the countries in focus. Country names are highlighted in their risk color as they appear. The text then fades out, and the same countries emerge as circular node badges arranged in a circle, connected by SVG edges weighted by co-occurrence. The graph is interactive once the full animation completes.

Exploding Paragraph. The top six headlines appear stacked in the center. The block shakes, then each headline flies outward to a position around the screen. The headlines then cluster toward their primary country's node, and the country constellation fades in beneath them. This one is reserved for contexts where there is more time to breathe.

Both animations derive all their content from the same live data the app uses — useGeminiTopics(), cached in localStorage for one hour. On repeat visits, the data is available instantly. On a cold first visit, a simple dark spinner shows until the first fetch resolves.

// The graph is built from topic data in one pass
function buildGraph(topics) {
  const nodeMap = {}, edgeMap = {};
  for (const topic of topics) {
    const codes = topic.regions
      .map(r => regionToCountryCode(r))
      .filter(Boolean);
    for (const code of codes) {
      nodeMap[code] ??= { code, count: 0, headlines: [] };
      nodeMap[code].count++;
    }
    // Edge weight = number of topics that mention both countries
    for (let i = 0; i < codes.length; i++)
      for (let j = i + 1; j < codes.length; j++) {
        const key = [codes[i], codes[j]].sort().join('-');
        edgeMap[key] ??= { from: codes[i], to: codes[j], weight: 0 };
        edgeMap[key].weight++;
      }
  }
  // Top 8 countries by article count, risk level derived from count
  const nodes = Object.values(nodeMap)
    .sort((a, b) => b.count - a.count)
    .slice(0, 8)
    .map(n => ({ ...n, risk: n.count >= 4 ? 'high' : n.count >= 3 ? 'elevated' : n.count >= 2 ? 'moderate' : 'low' }));
  return { nodes, edges: Object.values(edgeMap).filter(e => ...) };
}

The Phase Pattern

Both animations use the same timing architecture: a chain of setTimeout calls that advance a phase counter, with each phase triggering CSS class changes that drive transitions and keyframe animations.

useEffect(() => {
  const t = [
    setTimeout(() => setPhase(1), 200),    // text appears
    setTimeout(() => setPhase(2), 2000),   // text shakes
    setTimeout(() => setPhase(3), 2600),   // words scatter
    setTimeout(() => setPhase(4), 4200),   // nodes appear
    setTimeout(() => setPhase(5), 5800),   // hover enabled
  ];
  return () => t.forEach(clearTimeout);
}, []);

This keeps the animation logic declarative and easy to tune. Each phase is a CSS state, not an imperative animation loop. The cleanup function ensures no state updates fire after the component unmounts — important because the loading screen disappears as soon as data arrives.

What Pretext Is Actually Good For

Walking through this process clarified something: Pretext solves a different problem than what we had. It shines when you need precise, reflow-aware text layout — a document editor, a data visualization where labels must not overlap, or a generative design tool where text and graphics are computed together frame by frame. Those are contexts where you are in control of the full render cycle and can afford the setup cost.

A loading screen is the wrong host for that kind of system. It needs to be lightweight, fast to initialize, and resilient to uncertain data. CSS keyframes and React state transitions are exactly right for that job.

We are keeping Pretext on the list for future use. There are places in the app — country map labels, thread timeline annotations — where its obstacle-aware layout could genuinely improve the experience. When we get there, we will write about that too.

The final component — IntelligenceLoader — is a self-contained React component with two props: type="typewriter" and type="explode". It handles its own data fetching, fallback states, and cleanup. Any page in the app can add a cinematic loading screen with one line.

Global Perspectives uses AI to track how global stories evolve across days, with country-level risk signals and narrative thread intelligence.

Open the app