January 22, 2026
Teaching an SVG to fire
The little animated neural network from the home hero, taken apart — signals travelling along synapses and nodes pulsing, in pure SVG and CSS.
The animations are illustrative — built for presentation purposes.
The small animated network on the home page started as a doodle: could I make a neural net feel like it’s thinking using nothing but SVG paths and CSS keyframes — no JavaScript at all? This is that doodle, on its own.
There are two tricks doing all the work. The signals travelling along the edges
are a single dash pushed around each path: set pathLength="1" so every path is
normalised to the same length regardless of its real geometry, give it a short
stroke-dasharray, and animate stroke-dashoffset — the dash slides along like
a packet moving between neurons. Staggering the delays across a few paths makes
it look like traffic rather than a metronome.
The nodes just pulse — a scale-and-opacity keyframe on the “hot” ones — with the same offset delays so the pulse looks like it’s caused by the arriving signal. It isn’t, of course; they’re independent animations that happen to rhyme. That’s most of what stylised motion is: not simulation, just a handful of cues arranged so the eye tells itself a story.
Like everything else on the site it takes its colours from the theme tokens, so
it’s violet-on-dark or violet-on-light for free, and prefers-reduced-motion
freezes it into a calm static diagram. Cheap, tiny, and it scratched the itch.
The whole experiment fits in one HTML file — copy it or download it:
Source code — neural-net.html(131 lines)Download .html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neural net — SVG + CSS, no JavaScript</title>
<!--
A tiny animated neural network in pure SVG + CSS — no JavaScript.
From slavkobabic.com/experiments — copy freely.
The two tricks:
- signals: a short dash pushed around each path (pathLength="1" normalises
every path to the same length; animating stroke-dashoffset slides the dash)
- nodes: a scale/opacity pulse with the same staggered delays, so the pulse
*looks* caused by the arriving signal.
-->
<style>
:root {
color-scheme: dark;
--bg: #0f0e17;
--bg-raised: #1a1826;
--fg-muted: #a09cb0;
--border: #312e40;
--accent: #a78bfa;
--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-muted: #5a5568;
--border: #dedbe8;
--accent: #6d28d9;
}
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: var(--bg);
}
.net {
width: min(90vw, 480px);
height: auto;
}
.edge {
stroke: var(--border);
stroke-width: 1.2;
fill: none;
}
/* Signal packets travelling along a synapse: a short dash pushed around
the normalized (pathLength=1) path. */
.flow {
stroke: var(--accent);
stroke-width: 1.6;
fill: none;
stroke-dasharray: 0.12 0.88;
animation: netflow 3.2s linear infinite;
opacity: 0.9;
}
.flow.f2 { animation-delay: -1.1s; }
.flow.f3 { animation-delay: -2.2s; }
.node {
fill: var(--bg-raised);
stroke: var(--fg-muted);
stroke-width: 1.2;
}
.node.hot {
fill: var(--accent);
stroke: var(--accent);
transform-box: fill-box;
transform-origin: center;
animation: netpulse 3.2s var(--ease-out) infinite;
}
.node.hot.p2 { animation-delay: -1.1s; }
.node.hot.p3 { animation-delay: -2.2s; }
@keyframes netflow {
to { stroke-dashoffset: -1; }
}
@keyframes netpulse {
0%, 100% { transform: scale(1); opacity: 0.75; }
50% { transform: scale(1.35); opacity: 1; }
}
/* Reduced motion: freeze into a calm static diagram. */
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; }
}
</style>
</head>
<body>
<svg class="net" viewBox="0 0 300 220" aria-hidden="true">
<path class="edge" d="M40 40 L150 30" />
<path class="edge" d="M40 40 L150 110" />
<path class="edge" d="M40 110 L150 30" />
<path class="edge" d="M40 110 L150 110" />
<path class="edge" d="M40 180 L150 110" />
<path class="edge" d="M40 180 L150 190" />
<path class="edge" d="M150 30 L260 75" />
<path class="edge" d="M150 110 L260 75" />
<path class="edge" d="M150 110 L260 145" />
<path class="edge" d="M150 190 L260 145" />
<path class="flow" d="M40 40 L150 30 L260 75" pathLength="1" />
<path class="flow f2" d="M40 110 L150 110 L260 145" pathLength="1" />
<path class="flow f3" d="M40 180 L150 110 L260 75" pathLength="1" />
<circle class="node" cx="40" cy="40" r="7" />
<circle class="node hot" cx="40" cy="110" r="7" />
<circle class="node" cx="40" cy="180" r="7" />
<circle class="node hot p2" cx="150" cy="30" r="8" />
<circle class="node hot p3" cx="150" cy="110" r="8" />
<circle class="node" cx="150" cy="190" r="8" />
<circle class="node hot" cx="260" cy="75" r="7" />
<circle class="node hot p2" cx="260" cy="145" r="7" />
</svg>
</body>
</html>