AI Thinking Loader — Free HTML CSS JS Chat Loading Snippet

AI Thinking Loader · Loaders · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Spinning gradient orb
A conic ring signals the AI is working.
Shimmering status
Clipped-gradient sheen sweeps the label.
Cycling phrases
Narrated progress with smooth fades.
Shimmer skeleton
Staggered bars preview the answer.
Three coordinated motions
Orb, text, and skeleton animate together.
Pure-CSS animation
JS only swaps phrases.
Chat-ready layout
Drops in as a message placeholder.
Fully themeable
Colors, phrases, and timing are tunable.

About this UI Snippet

AI Thinking Loader — A Chat Loading State That Feels Alive

Screenshot of the AI Thinking Loader snippet rendered live

The AI thinking loader is the loading state that has become standard in chat and assistant interfaces: a gradient orb spins, a shimmering "Thinking…" label cycles through status phrases, and a skeleton of shimmer bars hints at the response forming. This snippet builds the whole thing with plain HTML, CSS, and a small vanilla JavaScript phrase cycler — a far more engaging wait than a bare spinner.

The spinning gradient orb

The orb is a circle filled with a four-color conic-gradient that rotates via the alSpin keyframe, with an ::after pseudo-element inset by 4px and filled with the bubble background — turning the solid disc into a glowing ring. This is the classic "AI is working" emblem: a rainbow ring spinning beside the message. It is pure CSS and costs only one rotating element.

Shimmering status text

The status label uses the same clipped-gradient shimmer technique as polished "shiny text": the text is transparent and filled with a gradient that is mostly muted gray with a bright band, animated across the letters so a sheen sweeps through the word. That motion makes the status feel active — the assistant is not just stuck on a static label, it is visibly working. The shimmer is pure CSS via background-clip: text.

Cycling status phrases

Real assistants reassure users by narrating progress, so JavaScript rotates the label through phrases like "Reading context…", "Drafting a response…", and "Checking facts…" on an interval. Each change fades the label out and back in (a quick opacity transition) so the phrases swap smoothly rather than snapping. Communicating steps, even simulated ones, makes a wait feel shorter and more trustworthy than a single unchanging word.

The shimmer skeleton

Below the status, three skeleton bars of decreasing width preview the shape of the incoming answer. Each bar has a moving gradient (alWave) that sweeps a lighter band across it, with staggered animation-delays so the shimmer ripples down the lines. This is the standard content-placeholder technique, here signaling that text is about to stream in — pairing the orb (it is thinking) with the skeleton (a response is coming).

Three coordinated motions, all CSS

The orb spin, the text shimmer, and the skeleton wave are independent CSS animations running together, which is what makes the loader feel rich without any animation library. JavaScript only handles the discrete job of swapping phrases, keeping the per-frame work entirely on the compositor.

Customizing it

Edit the PHRASES array to match your assistant voice, recolor the orb gradient and the shimmer band, change the number and widths of skeleton bars, or retime any of the loops. Drop it into a chat thread as the assistant message placeholder, then replace it with the real streamed response when it arrives. Pair it with an ai chat interface or a typing indicator for a complete conversation UI.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA chat bubble shows a spinning orb and a shimmering status.
  2. 2
    Watch the orbA rainbow gradient ring spins beside the text.
  3. 3
    Read the statusThe shimmering label cycles through progress phrases.
  4. 4
    See the skeletonShimmer bars ripple to preview the incoming answer.
  5. 5
    Edit the phrasesChange the PHRASES array to your assistant voice.
  6. 6
    Swap in the responseReplace the loader with the streamed reply.

Real-world uses

Common Use Cases

AI chat placeholders
The waiting state in an ai chat interface.
Assistant widgets
Generation screens
Show progress while content is produced.
Search with AI
A richer state than a plain typing indicator.
Prompt composers
Follow an ai prompt composer submit.
Loading-state demos
A reference for multi-part AI loaders.

Got questions?

Frequently Asked Questions

The orb is a circle filled with a four-color conic-gradient that rotates on a keyframe, with an ::after pseudo-element inset by 4px and filled with the bubble background. That turns the solid disc into a glowing rainbow ring, the familiar AI-working emblem, using just one rotating element in pure CSS.

The label is transparent and filled with a gradient via background-clip: text — mostly muted gray with a bright band — animated across the letters so a sheen sweeps through the word. The motion makes the status feel active and working rather than a static stuck label, and it is entirely CSS.

Narrating progress reassures users and makes a wait feel shorter. JavaScript rotates the label through phrases like Reading context and Drafting a response on an interval, fading each out and back in so they swap smoothly. Even simulated steps feel more trustworthy than a single unchanging word.

Three bars of decreasing width with a moving shimmer gradient and staggered delays preview the shape of the incoming answer. Combined with the spinning orb, it pairs the message that the assistant is thinking with the message that a response is about to stream in, which sets the right expectation during the wait.

Render it as a chat message placeholder component and keep the phrase index in state, updating it on an interval set up in a mount effect with cleanup. The orb, shimmer, and skeleton are pure CSS. Swap the component for the real response when streaming begins. In Tailwind, define the spin, shine, and wave keyframes in the config and apply them with utilities.

AI Thinking Loader — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI Thinking Loader</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
    
    .al-bubble{display:flex;gap:14px;align-items:flex-start;max-width:360px;width:100%;background:#13131f;border:1px solid #232336;border-radius:16px;padding:16px 18px}
    
    .al-orb{width:34px;height:34px;border-radius:50%;flex-shrink:0;background:conic-gradient(from 0deg,#6366f1,#ec4899,#22d3ee,#6366f1);animation:alSpin 2.4s linear infinite;position:relative}
    .al-orb::after{content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f}
    @keyframes alSpin{to{transform:rotate(360deg)}}
    
    .al-lines{flex:1;padding-top:2px}
    .al-status{display:inline-block;font-size:13.5px;font-weight:600;
      background:linear-gradient(90deg,#6b6b85 0%,#6b6b85 35%,#fff 50%,#6b6b85 65%,#6b6b85 100%);
      background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;
      animation:alShine 1.8s linear infinite}
    @keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}
    
    .al-skeleton{display:flex;flex-direction:column;gap:7px;margin-top:11px}
    .al-skeleton span{height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite}
    .al-skeleton span:nth-child(1){width:92%}
    .al-skeleton span:nth-child(2){width:78%;animation-delay:.15s}
    .al-skeleton span:nth-child(3){width:60%;animation-delay:.3s}
    @keyframes alWave{to{background-position:-340% 0}}
  </style>
</head>
<body>
  <div class="al-stage">
    <div class="al-bubble">
      <div class="al-orb" aria-hidden="true"></div>
      <div class="al-lines">
        <span class="al-status" id="alStatus">Thinking…</span>
        <div class="al-skeleton"><span></span><span></span><span></span></div>
      </div>
    </div>
  </div>
  <script>
    // Cycle through realistic status phrases while the AI "works", so the loader
    // communicates progress rather than just spinning.
    var status = document.getElementById('alStatus');
    var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
    var i = 0;
    setInterval(function () {
      i = (i + 1) % PHRASES.length;
      status.style.opacity = '0';
      setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
    }, 1900);
    status.style.transition = 'opacity .22s';
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI Thinking Loader</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes alSpin{to{transform:rotate(360deg)}}

    @keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

    @keyframes alWave{to{background-position:-340% 0}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
    }

    .al-orb::after {
      content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f
    }

    .al-skeleton span {
      height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite
    }

    .al-skeleton span:nth-child(1) {
      width:92%
    }

    .al-skeleton span:nth-child(2) {
      width:78%;animation-delay:.15s
    }

    .al-skeleton span:nth-child(3) {
      width:60%;animation-delay:.3s
    }
  </style>
</head>
<body>
  <div class="al-stage">
    <div class="flex gap-3.5 items-start max-w-[360px] w-full bg-[#13131f] border border-[#232336] rounded-2xl py-4 px-[18px]">
      <div class="al-orb w-[34px] h-[34px] rounded-full shrink-0 [background:conic-gradient(from_0deg,#6366f1,#ec4899,#22d3ee,#6366f1)] [animation:alSpin_2.4s_linear_infinite] relative" aria-hidden="true"></div>
      <div class="flex-1 pt-0.5">
        <span class="inline-block text-[13.5px] font-semibold [background:linear-gradient(90deg,#6b6b85_0%,#6b6b85_35%,#fff_50%,#6b6b85_65%,#6b6b85_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text text-transparent [animation:alShine_1.8s_linear_infinite]" id="alStatus">Thinking…</span>
        <div class="al-skeleton flex flex-col gap-[7px] mt-[11px]"><span></span><span></span><span></span></div>
      </div>
    </div>
  </div>
  <script>
    // Cycle through realistic status phrases while the AI "works", so the loader
    // communicates progress rather than just spinning.
    var status = document.getElementById('alStatus');
    var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
    var i = 0;
    setInterval(function () {
      i = (i + 1) % PHRASES.length;
      status.style.opacity = '0';
      setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
    }, 1900);
    status.style.transition = 'opacity .22s';
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to AIThinkingLoader.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}

.al-bubble{display:flex;gap:14px;align-items:flex-start;max-width:360px;width:100%;background:#13131f;border:1px solid #232336;border-radius:16px;padding:16px 18px}

.al-orb{width:34px;height:34px;border-radius:50%;flex-shrink:0;background:conic-gradient(from 0deg,#6366f1,#ec4899,#22d3ee,#6366f1);animation:alSpin 2.4s linear infinite;position:relative}
.al-orb::after{content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f}
@keyframes alSpin{to{transform:rotate(360deg)}}

.al-lines{flex:1;padding-top:2px}
.al-status{display:inline-block;font-size:13.5px;font-weight:600;
  background:linear-gradient(90deg,#6b6b85 0%,#6b6b85 35%,#fff 50%,#6b6b85 65%,#6b6b85 100%);
  background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;
  animation:alShine 1.8s linear infinite}
@keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

.al-skeleton{display:flex;flex-direction:column;gap:7px;margin-top:11px}
.al-skeleton span{height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite}
.al-skeleton span:nth-child(1){width:92%}
.al-skeleton span:nth-child(2){width:78%;animation-delay:.15s}
.al-skeleton span:nth-child(3){width:60%;animation-delay:.3s}
@keyframes alWave{to{background-position:-340% 0}}
`;

export default function AIThinkingLoader() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    // Cycle through realistic status phrases while the AI "works", so the loader
    // communicates progress rather than just spinning.
    var status = document.getElementById('alStatus');
    var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
    var i = 0;
    setInterval(function () {
      i = (i + 1) % PHRASES.length;
      status.style.opacity = '0';
      setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
    }, 1900);
    status.style.transition = 'opacity .22s';
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="al-stage">
        <div className="al-bubble">
          <div className="al-orb" aria-hidden="true"></div>
          <div className="al-lines">
            <span className="al-status" id="alStatus">Thinking…</span>
            <div className="al-skeleton"><span></span><span></span><span></span></div>
          </div>
        </div>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function AIThinkingLoader() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    // Cycle through realistic status phrases while the AI "works", so the loader
    // communicates progress rather than just spinning.
    var status = document.getElementById('alStatus');
    var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
    var i = 0;
    setInterval(function () {
      i = (i + 1) % PHRASES.length;
      status.style.opacity = '0';
      setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
    }, 1900);
    status.style.transition = 'opacity .22s';
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes alSpin{to{transform:rotate(360deg)}}

@keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

@keyframes alWave{to{background-position:-340% 0}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}

.al-orb::after {
  content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f
}

.al-skeleton span {
  height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite
}

.al-skeleton span:nth-child(1) {
  width:92%
}

.al-skeleton span:nth-child(2) {
  width:78%;animation-delay:.15s
}

.al-skeleton span:nth-child(3) {
  width:60%;animation-delay:.3s
}
      `}</style>
      <div className="al-stage">
        <div className="flex gap-3.5 items-start max-w-[360px] w-full bg-[#13131f] border border-[#232336] rounded-2xl py-4 px-[18px]">
          <div className="al-orb w-[34px] h-[34px] rounded-full shrink-0 [background:conic-gradient(from_0deg,#6366f1,#ec4899,#22d3ee,#6366f1)] [animation:alSpin_2.4s_linear_infinite] relative" aria-hidden="true"></div>
          <div className="flex-1 pt-0.5">
            <span className="inline-block text-[13.5px] font-semibold [background:linear-gradient(90deg,#6b6b85_0%,#6b6b85_35%,#fff_50%,#6b6b85_65%,#6b6b85_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text text-transparent [animation:alShine_1.8s_linear_infinite]" id="alStatus">Thinking…</span>
            <div className="al-skeleton flex flex-col gap-[7px] mt-[11px]"><span></span><span></span><span></span></div>
          </div>
        </div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="al-stage">
    <div class="al-bubble">
      <div class="al-orb" aria-hidden="true"></div>
      <div class="al-lines">
        <span class="al-status" id="alStatus">Thinking…</span>
        <div class="al-skeleton"><span></span><span></span><span></span></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

let status;
var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
var i = 0;

onMounted(() => {
  status = document.getElementById('alStatus');
  setInterval(function () {
    i = (i + 1) % PHRASES.length;
    status.style.opacity = '0';
    setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
  }, 1900);
  status.style.transition = 'opacity .22s';
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}

.al-bubble{display:flex;gap:14px;align-items:flex-start;max-width:360px;width:100%;background:#13131f;border:1px solid #232336;border-radius:16px;padding:16px 18px}

.al-orb{width:34px;height:34px;border-radius:50%;flex-shrink:0;background:conic-gradient(from 0deg,#6366f1,#ec4899,#22d3ee,#6366f1);animation:alSpin 2.4s linear infinite;position:relative}
.al-orb::after{content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f}
@keyframes alSpin{to{transform:rotate(360deg)}}

.al-lines{flex:1;padding-top:2px}
.al-status{display:inline-block;font-size:13.5px;font-weight:600;
  background:linear-gradient(90deg,#6b6b85 0%,#6b6b85 35%,#fff 50%,#6b6b85 65%,#6b6b85 100%);
  background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;
  animation:alShine 1.8s linear infinite}
@keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

.al-skeleton{display:flex;flex-direction:column;gap:7px;margin-top:11px}
.al-skeleton span{height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite}
.al-skeleton span:nth-child(1){width:92%}
.al-skeleton span:nth-child(2){width:78%;animation-delay:.15s}
.al-skeleton span:nth-child(3){width:60%;animation-delay:.3s}
@keyframes alWave{to{background-position:-340% 0}}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-a-i-thinking-loader',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="al-stage">
      <div class="al-bubble">
        <div class="al-orb" aria-hidden="true"></div>
        <div class="al-lines">
          <span class="al-status" id="alStatus">Thinking…</span>
          <div class="al-skeleton"><span></span><span></span><span></span></div>
        </div>
      </div>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a14;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
    
    .al-bubble{display:flex;gap:14px;align-items:flex-start;max-width:360px;width:100%;background:#13131f;border:1px solid #232336;border-radius:16px;padding:16px 18px}
    
    .al-orb{width:34px;height:34px;border-radius:50%;flex-shrink:0;background:conic-gradient(from 0deg,#6366f1,#ec4899,#22d3ee,#6366f1);animation:alSpin 2.4s linear infinite;position:relative}
    .al-orb::after{content:'';position:absolute;inset:4px;border-radius:50%;background:#13131f}
    @keyframes alSpin{to{transform:rotate(360deg)}}
    
    .al-lines{flex:1;padding-top:2px}
    .al-status{display:inline-block;font-size:13.5px;font-weight:600;
      background:linear-gradient(90deg,#6b6b85 0%,#6b6b85 35%,#fff 50%,#6b6b85 65%,#6b6b85 100%);
      background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;
      animation:alShine 1.8s linear infinite}
    @keyframes alShine{0%{background-position:120% 0}100%{background-position:-120% 0}}
    
    .al-skeleton{display:flex;flex-direction:column;gap:7px;margin-top:11px}
    .al-skeleton span{height:9px;border-radius:5px;background:linear-gradient(90deg,#1d1d2e 25%,#2c2c44 37%,#1d1d2e 63%);background-size:340% 100%;animation:alWave 1.4s ease-in-out infinite}
    .al-skeleton span:nth-child(1){width:92%}
    .al-skeleton span:nth-child(2){width:78%;animation-delay:.15s}
    .al-skeleton span:nth-child(3){width:60%;animation-delay:.3s}
    @keyframes alWave{to{background-position:-340% 0}}
  `]
})
export class AIThinkingLoaderComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    // Cycle through realistic status phrases while the AI "works", so the loader
    // communicates progress rather than just spinning.
    var status = document.getElementById('alStatus');
    var PHRASES = ['Thinking…', 'Reading context…', 'Drafting a response…', 'Checking facts…', 'Almost there…'];
    var i = 0;
    setInterval(function () {
      i = (i + 1) % PHRASES.length;
      status.style.opacity = '0';
      setTimeout(function () { status.textContent = PHRASES[i]; status.style.opacity = '1'; }, 220);
    }, 1900);
    status.style.transition = 'opacity .22s';
  }
}