May 30, 2026
AI in practice, in four loops
Four tiny looping vignettes of AI doing real work — RAG support, an editor suggestion, an agent run, a vector search — built entirely with CSS animation.
The animations are illustrative — built for presentation purposes.
Most “AI” visuals are stock imagery of glowing brains. I wanted the opposite: small, concrete scenes of the tools actually doing their unglamorous jobs. So I built four looping vignettes — no JavaScript, no canvas, just CSS keyframes timed against each other on a shared nine-second loop.
Support from a knowledge baseThe answer comes from the company's real documents — with a source, not from imagination.
AI in the editorThe assistant suggests, the engineer decides. Part of my everyday toolkit.
An agent takes over the routineA multi-step process quietly doing its job — a human watches the exceptions.
Vector searchThe question doesn't match keywords, it matches meaning — and finds the nearest knowledge.
Each one is a scene I’ve shipped some version of in production:
- RAG support — a question answered from a company’s own documents, with the sources cited underneath. The citation isn’t decoration; it’s the whole point.
- The editor — a ghost suggestion appearing and being accepted with Tab. The assistant proposes, the engineer decides.
- An agent run — a multi-step back-office process ticking through its steps and logging a clean result, with a human left to watch the exceptions.
- Vector search — a query landing in embedding space and lighting up its three nearest neighbours by meaning rather than by keyword.
The fun constraint was making every animation’s resting state its finished
state. Freeze the loop — or turn on prefers-reduced-motion — and each card
reads as a completed scene rather than a broken one: checkmarks shown, spinners
gone, the answer in place. Motion that degrades to a sensible still frame is a
small discipline, and it’s the difference between an animation that respects the
viewer and one that just demands attention.
All four vignettes fit in one HTML file — copy it or download it:
Source code — ai-in-practice.html(563 lines)Download .html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AI in practice — four loops, pure CSS animation</title>
<!--
Four looping vignettes of AI doing real work — RAG support, an editor
suggestion, an agent run, a vector search. No JavaScript, no canvas: just
CSS keyframes timed against each other on a shared nine-second loop.
From slavkobabic.com/experiments — copy freely.
Every animation's resting state is its finished state: freeze the loop (or
turn on prefers-reduced-motion) and each card reads as a completed scene.
-->
<style>
:root {
color-scheme: dark;
--bg: #0f0e17;
--bg-raised: #1a1826;
--fg: #d8d6e2;
--fg-strong: #f3f2f8;
--fg-muted: #a09cb0;
--border: #312e40;
--accent: #a78bfa;
--accent-contrast: #0f0e17;
--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
--bg: #fbfaff;
--bg-raised: #f1effa;
--fg: #1d1b26;
--fg-strong: #0e0d15;
--fg-muted: #5a5568;
--border: #dedbe8;
--accent: #6d28d9;
--accent-contrast: #fbfaff;
}
}
body {
margin: 0;
padding: 2.5rem 1rem;
background: var(--bg);
color: var(--fg);
font-family: system-ui, sans-serif;
line-height: 1.5;
}
.demos {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.5rem;
max-width: 920px;
margin-inline: auto;
}
@media (max-width: 720px) {
.demos { grid-template-columns: 1fr; }
}
.demo {
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
}
.titlebar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.9rem;
border-bottom: 1px solid var(--border);
background: var(--bg-raised);
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--fg-muted);
}
.lamp {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
animation: lamp 2.4s var(--ease-out) infinite;
}
@keyframes lamp {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
.stage {
flex: 1;
min-height: 172px;
padding: 0.95rem 1rem;
font-size: 0.8rem;
line-height: 1.5;
}
.caption {
margin: 0;
padding: 0.7rem 1rem 0.85rem;
border-top: 1px dashed var(--border);
font-size: 0.875rem;
color: var(--fg-muted);
}
.caption strong {
display: block;
margin-block-end: 0.1rem;
color: var(--fg-strong);
font-weight: 600;
}
/* ---- shared vignette building blocks ---- */
.bub {
display: inline-block;
max-width: 90%;
padding: 0.4rem 0.7rem;
border-radius: 0.8rem;
}
.bub.user {
background: var(--accent);
color: var(--accent-contrast);
border-bottom-right-radius: 0.25rem;
}
.bub.bot {
background: var(--bg-raised);
border: 1px solid var(--border);
border-bottom-left-radius: 0.25rem;
}
.row-r { margin-block-end: 0.55rem; text-align: right; }
.row-l { margin-block-end: 0.55rem; }
.dots {
display: inline-flex;
gap: 4px;
padding-block: 0.15rem;
}
.dots i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--fg-muted);
animation: blink 1s infinite ease-in-out;
}
.dots i:nth-child(2) { animation-delay: 0.18s; }
.dots i:nth-child(3) { animation-delay: 0.36s; }
@keyframes blink {
0%, 70%, 100% { opacity: 0.3; transform: translateY(0); }
35% { opacity: 1; transform: translateY(-2px); }
}
.chip {
display: inline-block;
margin-inline-end: 0.35rem;
padding: 0.08rem 0.45rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg-raised);
font-family: var(--font-mono);
font-size: 0.66rem;
color: var(--fg-muted);
}
/* ---- vignette 1: RAG chat (9s loop) ---- */
.v1-q { animation: v1q 9s infinite; }
.v1-dots { animation: v1dots 9s infinite; }
.v1-a { animation: v1a 9s infinite; }
.v1-src { animation: v1src 9s infinite; }
@keyframes v1q {
0% { opacity: 0; }
5% { opacity: 1; }
93% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes v1dots {
0%, 9% { opacity: 0; }
12%, 26% { opacity: 1; }
29%, 100% { opacity: 0; }
}
@keyframes v1a {
0%, 29% { opacity: 0; transform: translateY(4px); }
34% { opacity: 1; transform: none; }
93% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes v1src {
0%, 40% { opacity: 0; }
46% { opacity: 1; }
93% { opacity: 1; }
100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.v1-dots { display: none; }
}
/* ---- vignette 2: coding assistant (9s loop) ---- */
.code {
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.7;
}
.cl { display: flex; white-space: pre; }
.ln {
width: 2ch;
margin-inline-end: 1ch;
color: var(--fg-muted);
opacity: 0.55;
text-align: right;
user-select: none;
flex-shrink: 0;
}
.kw { color: var(--accent); }
.swap { display: grid; }
.swap > * { grid-area: 1 / 1; }
.ghost {
color: var(--fg-muted);
font-style: italic;
animation: v2ghost 9s infinite;
opacity: 0;
}
.solid { animation: v2solid 9s infinite; }
.accepted {
display: inline-block;
margin-block-start: 0.45rem;
font-family: var(--font-mono);
font-size: 0.64rem;
color: var(--accent);
animation: v2tab 9s infinite;
}
.caret {
display: inline-block;
width: 1px;
height: 0.95em;
background: var(--accent);
vertical-align: -0.15em;
animation: caret 1s steps(1) infinite;
}
@keyframes caret {
0%, 55% { opacity: 1; }
56%, 100% { opacity: 0; }
}
@keyframes v2ghost {
0%, 18% { opacity: 0; }
24%, 52% { opacity: 0.75; }
56%, 100% { opacity: 0; }
}
@keyframes v2solid {
0%, 55% { opacity: 0; }
60%, 94% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes v2tab {
0%, 58% { opacity: 0; }
63%, 88% { opacity: 1; }
94%, 100% { opacity: 0; }
}
/* ---- vignette 3: agent run (9s loop) ---- */
.steps {
display: grid;
gap: 0.55rem;
margin: 0;
padding: 0;
list-style: none;
}
.steps li {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--fg-muted);
}
.ico {
position: relative;
width: 14px;
height: 14px;
flex-shrink: 0;
}
.ring, .spin, .done {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.ring {
border: 1.5px solid var(--border);
border-radius: 50%;
}
.spin {
border: 1.5px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
opacity: 0;
}
.done {
color: var(--accent);
font-size: 11px;
line-height: 1;
opacity: 0;
}
@keyframes spinrot {
to { transform: rotate(360deg); }
}
.s1 .spin { animation: spinrot 0.8s linear infinite, v3s1spin 9s linear infinite; }
.s2 .spin { animation: spinrot 0.8s linear infinite, v3s2spin 9s linear infinite; }
.s3 .spin { animation: spinrot 0.8s linear infinite, v3s3spin 9s linear infinite; }
.s4 .spin { animation: spinrot 0.8s linear infinite, v3s4spin 9s linear infinite; }
.s1 .done { animation: v3s1done 9s infinite; }
.s2 .done { animation: v3s2done 9s infinite; }
.s3 .done { animation: v3s3done 9s infinite; }
.s4 .done { animation: v3s4done 9s infinite; }
@keyframes v3s1spin { 0%, 4% { opacity: 0; } 5%, 21% { opacity: 1; } 22%, 100% { opacity: 0; } }
@keyframes v3s1done { 0%, 21% { opacity: 0; } 22%, 95% { opacity: 1; } 100% { opacity: 0; } }
@keyframes v3s2spin { 0%, 22% { opacity: 0; } 23%, 39% { opacity: 1; } 40%, 100% { opacity: 0; } }
@keyframes v3s2done { 0%, 39% { opacity: 0; } 40%, 95% { opacity: 1; } 100% { opacity: 0; } }
@keyframes v3s3spin { 0%, 40% { opacity: 0; } 41%, 57% { opacity: 1; } 58%, 100% { opacity: 0; } }
@keyframes v3s3done { 0%, 57% { opacity: 0; } 58%, 95% { opacity: 1; } 100% { opacity: 0; } }
@keyframes v3s4spin { 0%, 58% { opacity: 0; } 59%, 75% { opacity: 1; } 76%, 100% { opacity: 0; } }
@keyframes v3s4done { 0%, 75% { opacity: 0; } 76%, 95% { opacity: 1; } 100% { opacity: 0; } }
.agent-log {
margin-block-start: 0.7rem;
font-family: var(--font-mono);
font-size: 0.64rem;
color: var(--accent);
animation: v3log 9s infinite;
}
@keyframes v3log {
0%, 76% { opacity: 0; }
80%, 94% { opacity: 1; }
100% { opacity: 0; }
}
/* ---- vignette 4: vector search (9s loop) ---- */
.vec { width: 100%; height: auto; }
.pt {
fill: var(--fg-muted);
opacity: 0.45;
}
.hit {
fill: var(--accent);
transform-box: fill-box;
transform-origin: center;
animation: v4hit 9s infinite;
}
.qry {
fill: var(--accent);
transform-box: fill-box;
transform-origin: center;
animation: v4qry 9s infinite;
}
.ripple {
fill: none;
stroke: var(--accent);
stroke-width: 1.5;
transform-box: fill-box;
transform-origin: center;
opacity: 0;
animation: v4rip 9s infinite;
}
.link {
stroke: var(--accent);
stroke-width: 1.2;
fill: none;
stroke-dasharray: 1;
animation: v4link 9s infinite;
}
.vec-label {
font-family: var(--font-mono);
font-size: 0.64rem;
color: var(--fg-muted);
animation: v4lbl 9s infinite;
}
@keyframes v4qry {
0%, 6% { opacity: 0; transform: scale(0.4); }
12%, 94% { opacity: 1; transform: scale(1); }
100% { opacity: 0; }
}
@keyframes v4rip {
0%, 13% { opacity: 0; transform: scale(0.3); }
18% { opacity: 0.9; transform: scale(1); }
34% { opacity: 0; transform: scale(3.2); }
100% { opacity: 0; }
}
@keyframes v4hit {
0%, 34% { opacity: 0.45; fill: var(--fg-muted); transform: scale(1); }
42%, 94% { opacity: 1; fill: var(--accent); transform: scale(1.45); }
100% { opacity: 0.45; }
}
@keyframes v4link {
0%, 42% { stroke-dashoffset: 1; opacity: 0; }
46% { opacity: 1; }
60%, 92% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 0; opacity: 0; }
}
@keyframes v4lbl {
0%, 60% { opacity: 0; }
66%, 93% { opacity: 1; }
100% { opacity: 0; }
}
/* Reduced motion: every card reads as a completed static scene. */
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; }
.done, .agent-log, .solid, .accepted { opacity: 1; }
.spin, .ghost, .ripple { opacity: 0; }
}
</style>
</head>
<body>
<div class="demos">
<!-- ------------- 1. RAG support chat ------------- -->
<article class="demo" aria-label="Support from a knowledge base">
<div class="titlebar" aria-hidden="true"><span class="lamp"></span>support.internal · RAG</div>
<div class="stage" aria-hidden="true">
<div class="row-r v1-q"><span class="bub user">How long is the warranty on the X200?</span></div>
<div class="row-l v1-dots">
<span class="bub bot"><span class="dots"><i></i><i></i><i></i></span></span>
</div>
<div class="row-l v1-a"><span class="bub bot">The X200 warranty runs 36 months from the date of purchase and covers parts and labour.</span></div>
<div class="row-l v1-src">
<span class="chip">[warranty.pdf · p. 4]</span>
<span class="chip">[pricelist-2026.xlsx]</span>
</div>
</div>
<p class="caption">
<strong>Support from a knowledge base</strong>
The answer comes from the company's real documents — with a source, not from imagination.
</p>
</article>
<!-- ------------- 2. Coding assistant ------------- -->
<article class="demo" aria-label="AI in the editor">
<div class="titlebar" aria-hidden="true"><span class="lamp"></span>invoice_service.ts · editor</div>
<div class="stage" aria-hidden="true">
<div class="code">
<div class="cl"><span class="ln">14</span><span><span class="kw">async</span> sendReminders() {</span></div>
<div class="cl"><span class="ln">15</span><span> <span class="kw">const</span> overdue = <span class="kw">await</span> this.repo.findOverdue();<span class="caret"></span></span></div>
<div class="swap">
<div class="ghost">
<div class="cl"><span class="ln">16</span><span> for (const inv of overdue) {</span></div>
<div class="cl"><span class="ln">17</span><span> await this.mailer.remind(inv);</span></div>
</div>
<div class="solid">
<div class="cl"><span class="ln">16</span><span> <span class="kw">for</span> (<span class="kw">const</span> inv <span class="kw">of</span> overdue) {</span></div>
<div class="cl"><span class="ln">17</span><span> <span class="kw">await</span> this.mailer.remind(inv);</span></div>
</div>
</div>
</div>
<span class="accepted">Tab · suggestion accepted ✓</span>
</div>
<p class="caption">
<strong>AI in the editor</strong>
The assistant suggests, the engineer decides. Part of my everyday toolkit.
</p>
</article>
<!-- ------------- 3. Agent run ------------- -->
<article class="demo" aria-label="An agent takes over the routine">
<div class="titlebar" aria-hidden="true"><span class="lamp"></span>agent/order-processing · run #4821</div>
<div class="stage" aria-hidden="true">
<ul class="steps" role="presentation">
<li class="s1"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Fetch new orders from the inbox</li>
<li class="s2"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Extract items and amounts (LLM)</li>
<li class="s3"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Reconcile against the ERP</li>
<li class="s4"><span class="ico"><span class="ring"></span><span class="spin"></span><span class="done">✓</span></span>Post and archive the document</li>
</ul>
<div class="agent-log">✓ 4/4 steps · 12 orders processed · 0 errors</div>
</div>
<p class="caption">
<strong>An agent takes over the routine</strong>
A multi-step process quietly doing its job — a human watches the exceptions.
</p>
</article>
<!-- ------------- 4. Vector search ------------- -->
<article class="demo" aria-label="Vector search">
<div class="titlebar" aria-hidden="true"><span class="lamp"></span>embeddings · top-k=3</div>
<div class="stage" aria-hidden="true">
<svg class="vec" viewBox="0 0 280 120">
<circle class="pt" cx="36" cy="30" r="4"></circle>
<circle class="pt" cx="70" cy="92" r="4"></circle>
<circle class="pt" cx="118" cy="18" r="4"></circle>
<circle class="pt" cx="236" cy="100" r="4"></circle>
<circle class="pt" cx="256" cy="28" r="4"></circle>
<circle class="hit" cx="172" cy="38" r="4"></circle>
<circle class="hit" cx="196" cy="72" r="4"></circle>
<circle class="hit" cx="148" cy="84" r="4"></circle>
<path class="link" d="M150 60 L172 38" pathLength="1"></path>
<path class="link" d="M150 60 L196 72" pathLength="1"></path>
<path class="link" d="M150 60 L148 84" pathLength="1"></path>
<circle class="ripple" cx="150" cy="60" r="8"></circle>
<circle class="qry" cx="150" cy="60" r="5"></circle>
</svg>
<div class="vec-label">query → 3 nearest documents · cos ≥ 0.87</div>
</div>
<p class="caption">
<strong>Vector search</strong>
The question doesn't match keywords, it matches meaning — and finds the nearest knowledge.
</p>
</article>
</div>
</body>
</html>