thinking-orbsとは?AIエージェントは今何をしているのかを表示するReact用ツールを試してみた動いた
この日に実際に動かした結果です。内容の誤りに気づかれた場合はお問い合わせください。確認のうえ訂正します。
使う前に分かること
| 必要なもの | Node.js、React 18以上 |
|---|---|
| 動かした環境 | Node.js 22、React 18 + TypeScript、Chromium(Playwright)。 |
| 日本語 | 画面・文書は英語 |
| 確認日 | 2026-09-21(この日に実際に動かした) |
AIエージェントが考えている間の画面って、たいていただのくるくる回るスピナーです。それで用は足りるのですが、検索しているのか、コードを解いているのか、話を聞いているのかが見た目で区別できたら少し親切だな、と思うことがあります。thinking-orbsは、そのためだけに作られたReact用の部品で、9種類の動きが最初から用意されています。Reactというのは、ウェブの画面を「部品」の組み合わせで作る仕組みで、thinking-orbsはその部品の1つです。使い方は、自分のReactアプリの画面の中に<ThinkingOrb state="searching" size={64} />という1行を置くと、そこに球が出る。AIが検索している間はsearching、考えている間はbreathingのように、状態に合わせてstateの中身を切り替える、それだけです。AIが今何をしているかの判定は、この部品の仕事ではありません。 AIを動かしているアプリ側が「今は検索中」とstateで教えてやる必要があり、thinking-orbsはそれを描くだけです。
自分のアプリがReactで作られていない人には、この部品を置く場所がありません。今回はReactアプリに実際に組み込んで、9種類を全部動かし、ダーク/ライトの自動切り替えや、動きを止める、読み上げ用の文言を差し替えるといった細かい制御まで一通り試しました。
入れて最初の1つを出す
まず、試すための空っぽのReactアプリを1つ作ります(最初の行のviteは、そのためのツールです)。すでに自分のReactアプリがある人はこの1行目は要らず、そのアプリのフォルダでnpm install thinking-orbsからです。
npm create vite@latest demo-app -- --template react-ts
cd demo-app
npm install
npm install thinking-orbs
added 27 packages, and audited 28 packages in 12s
found 0 vulnerabilities
added 4 packages, and audited 32 packages in 1s
found 0 vulnerabilities
src/App.tsxは、画面に何を出すかを書くファイルです。これをこう書き換えます。<ThinkingOrb state="searching" size={64} />の1行が「検索中の球を64pxで出せ」という意味で、上のimportの行はその部品をthinking-orbsから持ってくる宣言です。
import { ThinkingOrb } from 'thinking-orbs'
function App() {
return (
<div style={{ padding: 40 }}>
<ThinkingOrb state="searching" size={64} />
</div>
)
}
export default App
npm run dev -- --port 5183 --host 127.0.0.1
VITE v8.3.0 ready in 415 ms
➜ Local: http://127.0.0.1:5183/
npm run devで、手元だけで動く確認用のサーバーが立ちます。ブラウザでこのアドレスを開くと、白い点で組まれた小さな球が、ゆっくり点が動きながら浮かんでいました。
9つの状態を並べて見る
用意されている状態は9つで、それぞれ見た目も既定の読み上げ文言(aria-label)も違います。
| state | 見た目 | 既定のaria-label |
|---|---|---|
| working | 傾いた軌道を粒子が回る | Working… |
| searching | ドットの地球儀を走査線が掃く | Searching… |
| solving | 帯がスクランブルしてから揃う | Solving… |
| listening | 波形がリングを通り抜ける | Listening… |
| connecting | 星座のように線がつながっていく | Connecting… |
| weaving | 3本の筋が球面に編み込まれる | Weaving… |
| composing | うねる多層の帯 | Composing… |
| breathing | リングがゆっくり形を変える | Thinking… |
| shaping | 円・三角・四角の間で輪郭が変形する | Shaping… |
breathingだけ既定のaria-labelが「Breathing…」ではなく「Thinking…」でした。見た目の名前と読み上げ文言がずれている状態はこの1つだけです。
9つ全部を並べて描画してみます。
import { ThinkingOrb } from 'thinking-orbs'
const states = [
'working',
'searching',
'solving',
'listening',
'connecting',
'weaving',
'composing',
'breathing',
'shaping',
] as const
function App() {
return (
<div style={{ padding: 24, display: 'flex', flexWrap: 'wrap', gap: 32 }}>
{states.map((s) => (
<div key={s} style={{ textAlign: 'center', width: 100 }}>
<ThinkingOrb state={s} size={64} />
<div style={{ fontFamily: 'sans-serif', fontSize: 12, marginTop: 8 }}>{s}</div>
</div>
))}
</div>
)
}
export default App

どれもWebGLもフィルタも使わない、点だけの動きです。粒の数や速さは状態ごとに個別に調整されているようで、同じ「球」でもsearchingとsolvingでは点の並び方の癖がはっきり違って見えました。
暗い背景に自動で合わせる
themeプロパティを何も指定していないと、既定はautoです。ブラウザ側の配色設定をダークに切り替えて同じページを開き直すと、コード側は何も変えていないのに点の色が黒系から白系に反転しました。

自動判定はOSやブラウザの配色設定のほかに、親要素のdata-theme属性やdark/lightクラス(CSSフレームワークでよく使われる書き方)も見にいく作りだそうです。今回試したのはブラウザの配色設定を切り替えるほうだけで、data-theme属性を使う切り替えは試していません。
文中に小さく差し込む
用意されているサイズは64と20の2種類で、単純な縮小ではなく別々に調整されているとのことでした。文中に小さく差し込む使い方を試します。
import { ThinkingOrb } from 'thinking-orbs'
function App() {
return (
<div style={{ padding: 24, fontFamily: 'sans-serif' }}>
<p style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, margin: 0 }}>
エージェントが調べています <ThinkingOrb state="searching" size={20} />
</p>
<div style={{ marginTop: 16 }}>
<ThinkingOrb state="searching" size={64} />
</div>
</div>
)
}
export default App

文中の20pxのほうも、下の64pxのほうも同じsearching状態ですが、点の粒度がはっきり違います。64を単純に0.3倍しただけではなく、20px用に点の数と大きさを作り直しているのが見た目からも分かりました。
用意されているのが64と20の2つだけなら、間の値を渡したらどうなるか気になって試してみました。
<ThinkingOrb state="working" size={40} />
保存すると、このorbだけでなくページ全体が真っ白になり、開発者コンソールに次のエラーが出ました。
[Unhandled error] TypeError: Cannot read properties of undefined (reading 'count')
> Lt node_modules/thinking-orbs/dist/engine.es.js:542:4
> C node_modules/thinking-orbs/dist/index.es.js:87:43
sizeは64か20のどちらかしか受け付けず、それ以外の数値を渡すとページごと落ちます。TypeScriptの型としても64と20しか許可されていないので、型を守っていれば踏まない失敗ですが、数値の意味だけ見て「40くらいでもいいだろう」と書くと丸ごと描画が止まります。この行を消して保存すると、ページが自動で更新されて元の表示に戻りました。
止める
pausedプロパティをtrueにすると、その場で静止します。実際に止まっているか確かめるため、ページを開いた0.5秒後と、そこからさらに1.8秒後の2回、同じ画面をスクリーンショットして比べました。
import { ThinkingOrb } from 'thinking-orbs'
function App() {
return (
<div style={{ padding: 24, display: 'flex', gap: 60, fontFamily: 'sans-serif' }}>
<div style={{ textAlign: 'center' }}>
<ThinkingOrb state="solving" size={64} paused />
<div>paused=true</div>
</div>
<div style={{ textAlign: 'center' }}>
<ThinkingOrb state="solving" size={64} />
<div>paused=false</div>
</div>
</div>
)
}
export default App

paused=true側は2枚のスクリーンショットがバイト単位で完全に一致し、点の配置が1つも動いていませんでした。paused=false側は同じ2回の比較で点の配置がはっきり変わっていて、時間が経てば動く・pausedにすれば止まる、という単純な話がそのまま確認できました。
OS側の「視差効果を減らす」設定(prefers-reduced-motion: reduce)を有効にした状態でも同じ比較をしてみると、こちらもpausedと同じくスクリーンショットがバイト単位で一致し、静止したままでした。何もプロパティを足さなくても、読者側のアクセシビリティ設定だけで自動的に止まるようになっています。
読み上げ用の文言を差し替える
既定ではrole="img"と、状態ごとの英語のaria-label(前の表のとおり)が付きます。これを自前の日本語の文言に差し替えられます。
<ThinkingOrb state="solving" size={64} aria-label="リポジトリを解析中…" />
開発者ツールでこの要素を確認すると、aria-labelが指定した「リポジトリを解析中…」にそのまま置き換わっていました。既定の英語文言を消すために特別な設定は要らず、aria-labelを書き足すだけで上書きされます。
まとめ
9種類の動きは伊達ではなく、実際に並べてみると本当にそれぞれ違う癖の動き方をしていて、「検索中」と「考えている最中」を見た目で分けたいという最初の思いつきには十分応えてくれます。ダーク/ライトの自動判定やpaused、reduced motionへの対応まで最初から入っているので、自分のアプリに組み込むだけならほとんど詰まるところがありませんでした。唯一気になったのは、sizeに64か20以外の数値を渡すと、画面には何も出ずにページごと真っ白になる(エラーは開発者コンソールにしか出ない)点で、TypeScriptを使わずJavaScriptで雑に数値を渡すとハマりそうです。依存パッケージがゼロで導入も一瞬なので、AIエージェントのUIに状態表示を足したいだけなら、まず入れてみて損はないと思います。