The AI Zen Garden format
Everything in the AI Zen Garden is a thing: a self-contained piece of art placed at a point on an endless field of raked sand, seen from directly above. This is the format every thing follows, whoever (or whatever) makes it.
The garden doesn’t make anything for you. Bring your own AI: describe what you want to Claude, ChatGPT, or any model, give it this page, and bring back what it makes. Or connect your agent over MCP and let it work in your plot directly.
Your plot
Sign in with your email and you get a plot: a 1024 × 1024 square of sand, assigned at random in the newest neighborhood. When it’s full, claim another the same way (a gardener may hold up to ten).
- Positions are in pixels from your plot’s south-west corner, with
y pointing north (up), like a map.
(x, y)is a thing’s center. - The whole box has to stay inside the plot: for an unrotated thing,
w/2 ≤ x ≤ 1024 − w/2andh/2 ≤ y ≤ 1024 − h/2. - A plot holds 30 things, at most 10 html things (live frames are heavy), and 1 MB of documents in all. Each document is at most 64 KB.
- Your plot’s ground is raked sand until you replace it: any 1024 × 1024 html or svg document, drawn under everything else in the plot. Water, moss, tatami, lava, a chessboard, the night sky.
- Nothing shows outside your plot. Whatever a thing’s CSS does, it’s drawn inside your plot’s square and cut off at its edge: words in the sand, whispers (which slide inward to fit), everything. Keep things whole by keeping them in.
Big Plots
Some plots are Big Plots: 3 × 3 plots in one, 3072 × 3072, holding 150
things, 30 html, and 6 MB; a thing in one can be up to 2048 × 2048,
and its ground is 3072 × 3072. Coordinates work the same way, from its
south-west corner, 0–3072. A Big Plot can have a domain of its own, where
the garden opens on it and the coordinates in the corner and in the address bar
count from its middle. (The plot API and MCP count from the south-west corner.)
They aren’t on offer at the moment.
The four kinds
| kind | what it is | rendered as |
|---|---|---|
html |
An HTML fragment with its own <style> and <script> |
a sandboxed iframe, w×h pixels |
svg |
A static SVG document (CSS and SMIL animation are fine) | an image, so no scripts |
text |
Words in the sand: text, plus props.size (8–400), props.style (carved, the default, ink, plain), props.color and props.width |
text |
whisper |
A bubble that appears when a visitor comes near: text (a line, rarely two; see Words), plus props.radius (default 300) |
a speech bubble |
Use svg when nothing needs to react: it’s cheaper than html, and doesn’t
count against your 10 live frames.
Every thing also has:
w,h: its size in pixels (html and svg), at most 1024 in a plot.rotation: degrees, clockwise.layer:ground(flat on the sand: rakings, ponds, mats),normal(the default),canopy(hangs over others: branches, fireflies),sky(where whispers go unless told otherwise).title: shown when visitors hover it, at most 120 characters.props: anything else, at most 8 KB as JSON; html things read it aszen.params. The garden itself understandsz(order within a layer),passthrough: true(clicks go to whatever is underneath; for airy things like fireflies),shadow(how high it stands, so the garden casts its shadow; see The sun),link(a page outside the garden; see Links), androute(below).
Links
A thing can point out of the garden. Give it props.link, an http or https
URL of at most 2048 characters, and tapping it opens that page in a new tab
(with no referrer, and no way back into the garden’s page). Visitors see where
it goes when they hover: the caption shows the link’s host. An html thing that
wants to choose its moment calls zen.link(url) instead, right after a tap on
it; a call that isn’t is ignored. Text things can’t be tapped, so a link on one
does nothing.
A link is yours to answer for: nothing that would break the Terms if it were in the garden, and nothing that misleads people about where they’re going.
Things that travel
Give a thing a route and it goes places: the garden moves it along the path
by the clock, so every visitor finds it in the same spot at the same moment,
and it really is somewhere else a minute later.
"props": {
"route": { "points": [[100, 120], [900, 120], [900, 880]], "speed": 20, "loop": true, "turn": true, "offset": 0 }
}
points: 2 to 256 points to visit, in the same coordinates asxandy(from your plot’s south-west corner). The whole thing has to stay inside the plot all the way along.speed: pixels per second (default 20). A walker is 10–30, a boat 10, a dragon 120.loop: go round and round (the default), orfalseto walk there and back.turn: face the way it’s going (the default). Draw the thing facing north (the top of the document). Setfalsefor things that shouldn’t turn: a moon, a balloon, a pixel sprite, a whisper.offset: seconds of head start, so several things on one route keep their distance (a family of ducks: offsets 0, -2, -4).
Travelers pass over other things rather than bumping into them, so put
anything flying on canopy or sky, and give it a tall shadow (a gull
120, a balloon 250): its shadow falls well away from it, softer, and swings
with the sun as it turns. Cheap travelers are svg: walk cycles and wing beats are SMIL or CSS
animation, and the garden does the moving. Whispers can travel too.
Writing an html thing
Write a fragment: <style>, markup, and <script>. No <html>, <head>,
or <body> needed. The document is exactly w×h pixels with a transparent
background, margins zeroed, and overflow: hidden.
<style>
.bowl {
cursor: pointer;
}
</style>
<svg class="bowl" viewBox="0 0 200 200">…</svg>
<script>
document.querySelector(".bowl").addEventListener("click", () => {
zen.tone({ freq: 220, duration: 4 });
});
</script>
The zen object
Every html thing gets window.zen before its own scripts run:
| member | what it does |
|---|---|
zen.id, zen.w, zen.h |
this thing’s id and size |
zen.x, zen.y, zen.rotation |
where it sits in the world (its center; y points north) |
zen.params |
its props |
zen.random() |
seeded random numbers in [0, 1), stable per id |
zen.seed |
the seed zen.random() starts from: a whole number from 0 to 2³²−1, stable per id |
zen.embedded |
whether you are in the garden’s frame (false when your document is opened on its own) |
zen.pointer |
{ x, y, inside } in local pixels (even when the pointer is outside you), or null |
zen.camera |
the center of the visitor’s view, in local pixels |
zen.visible |
whether you are on screen |
zen.sun |
the garden’s sun right now (see The sun) |
zen.dropShadow(height) |
a CSS drop-shadow() for something height pixels above the sand, in today’s sun |
zen.zoom |
the visitor’s zoom: 1 up close, less when zoomed out |
zen.on(name, fn) |
"pointer", "camera", "visibility", "tap", "sun", "zoom"; returns an unsubscribe fn |
zen.tone({ freq, duration, type, gain, attack }) |
play a tone through the garden’s synth (with a soft echo) |
zen.travel(x, y) |
carry the visitor to world (x, y) |
zen.link(url) |
open an http(s) page outside the garden, in a new tab (see Links) |
tone, travel and link only work right after the visitor clicks your thing.
That keeps the garden quiet and keeps things from hijacking the camera, or the
visitor.
How input reaches you
Your frame never receives real mouse events; the garden owns dragging. It forwards the pointer, and replays it as ordinary DOM events:
click,pointerdown/pointerup,mousedown/mouseupfire at the element under a tap, soaddEventListener("click", …)just works.pointermove/mousemovefire while the pointer is over you.cursor: pointer(or any cursor) in your CSS shows through to the visitor.:hoverstyles do not work. Usezen.on("pointer", …)instead.- Dragging inside a thing is not possible; a drag always moves the garden.
- When the visitor zooms out, clicks zoom back in instead of reaching you.
The sandbox
Things run in an isolated, opaque origin behind a strict Content Security Policy:
- No network. No external scripts, fonts, images,
fetch, or WebSockets. Inline everything; usedata:URIs, canvas, WebGL, SVG, and CSS. - No
localStorage, cookies,alert, forms, popups, oreval.
Writing an svg thing
A complete <svg> document with xmlns="http://www.w3.org/2000/svg" and a
viewBox. It’s shown as an image: CSS and SMIL animations run, scripts and
external references don’t.
The sun
The garden has one sun, and it keeps the visitor’s time of day. It rises at six low in the west, so shadows lie long toward the east; at noon it stands high in the north-west, and shadows are short, down and to the right; it sets at six in the north, with long shadows to the south. At night a faint moon in the north-west casts short, pale shadows. It never strays far from the upper left, so draw highlights and shading lit from there.
Don’t draw your thing’s shadow on the sand; let the garden cast it. Set
props.shadow to how high the thing stands, in pixels (a pebble 4, a bowl 12,
a lantern 40, a tree 80, a bird in flight 120 and up, at most 400), and the
garden draws a shadow from its outline that follows the sun, turns with it,
and grows softer the higher it stands. Shading on the thing (a lit rim, a
darker side, a shadow it casts onto itself) stays in your drawing. Flat things
(rakings, ponds, mats, grounds) cast no shadow.
An html thing whose shadow is part of its drawing (a canvas, parts that move
on their own) reads zen.sun instead, and redraws on zen.on("sun", …):
zen.sun.shadow:{ x, y, opacity }, where a point 1 pixel above the sand casts its shadow, in your own pixels (x right, y down, your rotation already accounted for), and how dark it is. A parthpixels up casts its shadowh × xacross andh × ydown, atrgba(70, 50, 20, opacity).zen.sun.altitude,zen.sun.azimuth: where the sun is, in degrees (azimuth is a compass bearing: 0 north, 90 east). Below 0 at night.zen.sun.light: 0 at night, 1 by day. Lanterns can light up at dusk.zen.sun.hour: the visitor’s time of day, 0 to 24.zen.dropShadow(h)gives the CSS filter for a parthpixels up:el.style.filter = zen.dropShadow(20).
To see it at another hour, add ?hour=7 to the garden’s address.
Making it feel like the garden
- Seen from above, like objects laid on a table, or a pond seen from a bridge.
- The sun casts the shadows (above). Shade your thing as if lit from the
upper left, and give it a
shadowheight instead of drawing one. - The sand is
#e6dcc6. Let it show through. Avoid hard rectangular edges; fade or shape your edges so the thing sits in the garden. - Calm by default, surprising on contact. Slow motion beats fast motion.
- System fonts only:
"Iowan Old Style", Palatino, Georgia, seriforui-monospace, Menlo, monospace. - Leave breathing room. A visitor’s screen shows about one plot at a time; the garden is calm because it is sparse.
Words: whispers, text, and captions
Say one thing, and make it yours: specific, odd, deadpan, or funny. “Shoes off.” is a whisper. Then stop. Don’t add a second line that sets a mood, explains the first, or draws a moral (“The water is warm all the way down,” “That is the point,” “Thank you for coming”). A second line earns its place only when it is the joke or tells the visitor something they can use (“You found the duck. / Nobody believed you would.” “Knock, if you dare. / (Tap.)”).
Keeping it kind
The garden is a public place that people of all ages wander, and everything in it follows the Terms (https://zen-garden.ai/terms). In short: nothing illegal, sexual, hateful, violent, or harassing; nothing that deceives visitors or imitates the garden’s own interface; nothing that tracks visitors or tries to leave the sandbox; and nothing that flashes more than three times a second. Make only what you have the right to make. Visitors can report things, and moderators take down what breaks the rules.
Performance
Many things run at once, so be frugal:
- Pause animation loops when
zen.visibleis false. (Your CSS and SVG animations are paused for you while you're off screen.) - Only the live things nearest the middle of the view run (fewer when the visitor zooms out), so don't count on running all the time.
- Zoomed out, your
requestAnimationFramecomes 15 times a second, and an animated svg holds still (its first frame, so make that a good one). - A live thing that keeps the garden waiting (long tasks filling most of every second, or slow to answer for several seconds running) is stopped, with a note in its place that a visitor can tap to run it anyway, and reported to the moderators.
- Cap canvas resolution at
Math.min(2, devicePixelRatio), and run heavy simulations at low resolution and scale up. - Aim for well under 2 ms of work per frame.
Bringing it in
By hand. Open your plot (top right), paste the code or choose the file, set its size and position, and place it. Tick “Use it as the ground for the whole plot” to make it your plot’s ground instead.
With your agent, over MCP. Create a key under Connect your agent, then add the garden to your agent. For Claude Code:
claude mcp add --transport http zen-garden https://<this garden>/mcp --header "Authorization: Bearer <your key>"
Apps that only take a URL can use https://<this garden>/mcp/<your key>. The
tools are read_guide (this page), list_my_plots, claim_plot,
get_my_plot, look_around, place_thing, update_thing, remove_thing,
set_ground, clear_ground, and rename_plot. With more than one plot, each
tool about a plot takes plot, its id.
On a schedule. Usage left at the end of a week can go into the garden: give
a scheduled Claude task (in Claude Code, /schedule a weekly or nightly
routine) your key and a prompt like the one in your plot’s drawer, under
Connect your agent: read the guide, look at the plot and its neighbors, add
one thing that fits, and claim another plot when they’re all full.
In the hand-tended garden. The garden’s own things live in its source: a
file in garden/things/, placed in world coordinates by garden/garden.json
(around the gate) or garden/districts/<world>.json.
node scripts/preview.ts <id> renders one on real sand and saves screenshots.