An overlay arrow that slithers across the page like a snake — morphing smoothly from one bezier shape to another, taking as many crazy detours as you like, and steering like a car (it can't turn tighter than a minimum radius, so it always arrives pointing the right way).
Zero dependencies. One small ES module. Drop it on any page and point at things.
▶ Live demo — click anywhere to send the arrow there; tweak everything with the sliders.
Tooltips and onboarding hints usually just appear. Slarrow gives you an arrow with personality: it crawls across the screen from wherever it is to wherever you point it, lengthening and shrinking as it goes, optionally wandering through a few random spots first like it's a little lost — then nails the landing, head pointed exactly where you asked.
It's named for slither + arrow (and it started life as the Streamload arrow).
- Snake-like crawl — the body follows the head along a continuous path; it morphs from a start shape into an end shape.
- Car/truck/train steering — a configurable minimum turn radius (built on Dubins paths). It physically can't corner tighter than that, so it carves natural sweeping arcs and arrives head-on, never side-swiping the target.
- Detours — send it through N random spots first (looks "lost"), or pass explicit waypoints with per-point headings.
- Grow & shrink — independent start / mid / end body lengths, so it can swell long in the middle of a trip and shrink back at the end.
- Fully shapeable — thickness, tail shape (round → needle), arrowhead size (length & width in px), and an open chevron head (two lines) or a solid filled triangle.
- Drop-in overlay — renders into a fixed,
pointer-events: noneSVG layer, so it floats over your UI without intercepting clicks. - No dependencies, ~600 lines, MIT.
It's a single ES module with zero dependencies. Copy src/js/slarrow.js into your project:
<script type="module">
import { Slarrow } from './slarrow.js';
// ...
</script>Or pull it straight from a CDN (no build step):
import { Slarrow } from 'https://cdn.jsdelivr.net/gh/siverson914/slarrow/src/js/slarrow.js';import { Slarrow } from './slarrow.js';
const arrow = new Slarrow({ color: '#e1251b', width: 16, shadow: true });
// A "shape" is a cubic bezier: [tail, ctrl1, ctrl2, head].
// arrowAt() builds one whose head sits at {x,y} pointing EXACTLY at `angle`.
arrow.set(Slarrow.arrowAt({ x: 120, y: 120, angle: 0 }));
// Fly to a new shape, taking 3 random detours, steering like a car.
await arrow.flyTo(
Slarrow.arrowAt({ x: 700, y: 400, angle: Math.PI }),
{
detours: 3, // visit 3 random spots first
duration: 2600, // ms
turnRadius: 120, // px — it can't turn tighter than this
midLength: 480, // grow long mid-flight, then settle back
}
);Or send the head to a point and let it derive the end shape:
arrow.flyToPoint(event.clientX, event.clientY, { detours: 2, turnRadius: 90 });| option | default | meaning |
|---|---|---|
color |
'#e1251b' |
fill / stroke color |
width |
16 |
body thickness (px) |
shadow |
true |
soft drop shadow; or {dx,dy,blur,color,opacity} |
tail |
0.5 |
tail shape: 0 round/blunt → 1 sharp needle |
headLength |
28 |
arrowhead length (px) |
headWidth |
30 |
arrowhead width across (px) |
openHead |
false |
true = open chevron (two lines); false = filled triangle |
smoothness |
0.5 |
0..1 fallback turn-radius laziness when turnRadius isn't given |
samples |
90 |
body smoothness (points) |
container |
document.body |
element the overlay mounts into |
zIndex |
9999 |
overlay stacking |
All of these are also live properties (arrow.openHead = true, etc.); call set/flyTo to re-render.
arrow.set(shape)— snap to a shape instantly (no animation).arrow.flyTo(targetShape, opts)→Promise— animate to a shape. Opts:detours,waypoints,duration,turnRadius,midLength,smoothness,easing,seed.arrow.flyToPoint(x, y, opts)→Promise— fly the head to a point, auto-deriving the end shape.arrow.destroy()— remove the overlay.
Slarrow.arrowAt({ x, y, angle, length, curve })→ shape — a curved arrow whose head is at{x,y}pointing exactly atangle(radians,0= →).Slarrow.randomWaypoints(n, { width, height, padding, seed })→[[x,y,angle], …]— N random spots (passseedfor reproducibility).Slarrow.shapeLength(shape)→ px — arc length of a shape (handy for matchingmidLengthunits).
A shape is a cubic bezier as four [x, y] points: [tail, ctrl1, ctrl2, head]. The last point is the tip; the arrowhead points along the curve's tangent there.
A waypoint is [x, y] or [x, y, angle] — the optional angle pins the heading the snake must be travelling as it crosses that point.
The path between each pose (position + heading) is the shortest Dubins path: circular arcs of exactly the turn radius joined by straight runs. Because curvature never exceeds 1 / turnRadius, the arrow can't pivot in place — to reach a target behind it, it swings out in a wide arc (or a full loop) and comes back aligned. Crank turnRadius up and it behaves like a train needing a huge yard to turn around.
MIT — see LICENSE.
