ascii.rest/svg turns any piece, or a banner of your own text, into an animated SVG. The SVG plays where JavaScript can’t run, like a GitHub README or an <img> tag.
Three words you will see on this page. A piece is one animation, like donut or the rust logo. A banner is any text you choose, drawn in big block letters. A frame is the picture at one moment.
To make your first SVG:
-
Install the package:
npm install ascii.rest -
Save this script as
make-svg.ts:import { writeFileSync } from "node:fs"; import { bannerSvg } from "ascii.rest/svg"; writeFileSync("hello.svg", bannerSvg("hello", { color: ["#f97316", "#f778ba"] })); -
Run it. Node runs a
.tsfile as it is from Node 22.18, or 23.6 in Node 23. If yours prints an error, name the filemake-svg.mjsinstead. The script has no types, so it is plain JavaScript too.node make-svg.ts -
Open
hello.svgin your browser, or show it on a page with<img src="hello.svg" alt="hello">. It looks like this:
When to use an SVG
Use an SVG wherever the page can’t run JavaScript. <Ascii>, <ascii-art> and mount() draw each frame with JavaScript, so they can’t play in these places:
- A GitHub README. GitHub runs no scripts in a README, but it shows SVG images and plays their CSS animation.
- An
<img>tag. A browser never runs scripts inside an image, but it does play an SVG’s CSS animation. - Email. This depends on the email app. Apple Mail shows SVG images. Gmail turns them into a still PNG (caniemail). Test in the apps your readers use.
On a page that can run JavaScript, use react or html instead. They play the piece live, so it never has to loop.
The SVG holds each frame as text, and CSS shows the frames in turn, so it needs no script. The details are in how the SVG works.
svg() and bannerSvg() take values and return a string. They never touch a page, so they run anywhere JavaScript does: in Node, in a browser, in a Cloudflare Worker, or in a build script.
Turn a piece into an SVG
svg(piece, options) returns one piece as an SVG, in a string. Import the piece from ascii.rest/pieces by its export, in camelCase: nightCoast for the piece named night-coast.
import { svg } from "ascii.rest/svg";
import { donut, rust } from "ascii.rest/pieces";
const logo = svg(rust); // the rust logo, for a light page
const logoDark = svg(rust, { dark: true }); // the same logo, for a dark page
const spin = svg(donut, { ink: "#3fb950", seconds: 2 }); // a green donut that loops every 2 seconds
This is svg(rust). ascii.rest serves the same file at https://ascii.rest/svg/rust.svg:
| option | what it does | default |
|---|---|---|
dark |
Draws it for a dark page: light text, and a coloured piece’s dark colours. | false |
ink |
The colour of a text piece, as #rrggbb. A text piece is drawn in one colour, like donut. A coloured piece, like a logo, keeps its own colours. |
GitHub’s text colour: #1f2328, or #f0f6fc with dark |
seconds |
How long one loop lasts, in seconds. | the piece’s own loop, or 4: see make the loop seamless |
from |
The moment in the piece where the loop starts, in seconds. | the piece’s own loop, or 0 |
fps |
Frames it takes each second, from 1 to 60. More is smoother, and makes a bigger file. | 15 |
once |
Plays the loop once, then stays on its last frame. | false, or true for a banner with effect: "type" |
scale |
Pixels per column, when nothing else sets the image’s size. | 10 |
label |
What screen readers say. | "<name>, in ascii, from ascii.rest" |
options |
Options for the piece itself, like { scan: 3 } for a distro that scans every 3 seconds. |
the piece’s own |
A scene has a background colour of its own, its meta.ground. Its SVG draws that colour behind it, so it looks the same on a light page and a dark one. The same goes for a scene beside a banner in bannerSvg().
svg() throws an error when:
inkisn’t#rrggbb.fpsisn’t between 1 and 60.scaleisn’t a number of 0 or more.
Make the loop seamless
An SVG plays one loop over and over. If the piece looks different at the end of the loop than at the start, you see a jump. svg() picks a loop with no jump when it knows one:
- A banner: the time between two glints, or one play of its typing.
- A piece that sets
meta.loop: that many seconds. - A logo, or a company’s logo: the time between two glints. That is 5 seconds, unless you change the option for it listed in the logo’s
meta.options. - A Linux distro: the time between two scans, set by its
scanoption. A scan is a line that runs down the logo. - Anything else: 4 seconds. If the piece doesn’t repeat every 4 seconds, it jumps once a loop. Pass
secondsto change the length.
loopOf(piece) tells you which loop svg() will play:
import { loopOf } from "ascii.rest/svg";
import { donut, rust } from "ascii.rest/pieces";
loopOf(rust); // { every: 5, from: 4.5 }: 5 seconds, starting between two glints
loopOf(donut); // { every: 4, from: 0 }
A piece that doesn’t move, like box-frames, becomes a still SVG with no animation in it.
Make a banner SVG
bannerSvg(text, options) draws your text as a banner and returns it as an SVG string. It can also add a tagline under the letters, a piece beside them, and a coloured card behind them.
import { bannerSvg } from "ascii.rest/svg";
import { rust } from "ascii.rest/pieces";
const light = bannerSvg("ferris", { art: rust, color: "art", tagline: "fast, safe, fun" });
const dark = bannerSvg("ferris", { art: rust, color: "art", tagline: "fast, safe, fun", dark: true });
art: rust puts the rust logo beside the letters. color: "art" gives the letters the logo’s own colour. With no color, the letters are in GitHub’s text colour and the shadow is in GitHub’s muted grey, so the banner looks at home in a README.
bannerSvg() takes every option of banner(), such as font, shadow, fill, effect and speed. They are all on banners. These options are only for the SVG:
| option | what it does | default |
|---|---|---|
dark |
Draws it for a dark page. | false |
color |
The letters’ colour, as banner() takes it: one #rrggbb, a list of them for a fade, or { light, dark }. Or "art", for the art’s own colour, which is GitHub’s text colour when there is no art. |
GitHub’s text colour: #1f2328, or #f0f6fc with dark |
tagline |
A line of plain text under the letters. It types out once the letters are in, then a cursor blinks after it. | none |
taglineColor |
The tagline’s colour, as #rrggbb or { light, dark }. |
the shadow’s colour |
taglineSize |
The tagline’s font size, in SVG units. | 28 |
art |
A piece to show with the letters, like a logo. Or { svg }, an SVG that svg() made. |
none |
place |
Where the art goes: "left", "right", "above" or "below". |
"left" |
artSize |
The art’s height, as a multiple of the letters and tagline together. | 1 beside them, 2.4 above or below |
spacing |
The gap between the art and the letters, in SVG units. | 32 |
align |
Lines up the letters, the tagline and the art across the width: "start", "center" or "end". |
"center" when the art is above or below, else "start" |
background |
The colour of a card behind everything, as #rrggbb or { light, dark }. |
none |
padding |
The space between the card’s edge and what is on it, in SVG units. | 24 |
radius |
How round the card’s corners are, in SVG units. | 12 |
scale |
Pixels per column, when nothing else sets the image’s size. | 5 |
fps |
Frames it takes each second, from 1 to 60. | 15 |
label |
What screen readers say. | "<text>, in ascii, from ascii.rest", with : <tagline> after the text when there is one |
bannerSvg() throws an error when:
- The font can draw none of the text, like
"é"or"***". Text of only spaces counts as none. backgroundortaglineColorisn’t#rrggbb, for either theme, even one it doesn’t use.taglineSize,artSize,spacing,padding,radiusorscaleisn’t a number of 0 or more.placeisn’t"left","right","above"or"below", oralignisn’t"start","center"or"end".fpsisn’t between 1 and 60.artis{ svg }with an SVG that ascii.rest/svg didn’t write.- An option of
banner()is one it can’t take, like an unknown font. See banners.
Add a tagline
A tagline is a line of plain text under the letters. It types out after the letters appear, then a cursor blinks at its end. With effect: "still", it shows all at once, with no cursor.
import { bannerSvg } from "ascii.rest/svg";
const banner = bannerSvg("my project", { tagline: "a line about it", color: "#f97316" });
Change its colour with taglineColor and its size with taglineSize.
Put a piece beside the letters
art takes any piece. A logo works best, because its loop has no jump. place puts it on any side:
import { bannerSvg } from "ascii.rest/svg";
import { rust } from "ascii.rest/pieces";
const banner = bannerSvg("ferris", { art: rust, color: "art", place: "above" });
Beside the letters, the art is as tall as the letters and tagline together. Above or below them, it is 2.4 times as tall. Change that with artSize. spacing sets the gap between the art and the letters, and align lines them up across the width.
Put a card behind it
background draws a card with rounded corners behind everything. On a card, the colours follow the card, not the page: a dark card gets light letters, even without dark. So one SVG looks right on both light and dark pages.
import { bannerSvg } from "ascii.rest/svg";
import { rust } from "ascii.rest/pieces";
const banner = bannerSvg("ferris", { art: rust, color: "art", background: "#0d1117" });
padding sets the space inside the card’s edge, and radius rounds its corners.
Reuse a logo SVG you already have
art also takes { svg }: an SVG that svg() made, such as a logo from https://ascii.rest/svg/<name>.svg. bannerSvg() reuses that SVG’s frames instead of drawing the piece again. The result is the same SVG, made several times faster: see the times in keep the file small.
import { bannerSvg } from "ascii.rest/svg";
const rustSvg = await (await fetch("https://ascii.rest/svg/rust.svg")).text();
const banner = bannerSvg("ferris", { art: { svg: rustSvg }, color: "art" });
For a dark banner, use the dark logo, https://ascii.rest/svg/rust.dark.svg. An SVG that ascii.rest/svg didn’t write throws an error.
Set the size
An SVG stays sharp at any size. You can set its size in two places.
On the page, give the <img> a width. The height follows:
<img src="hello.svg" alt="hello" width="400">
In the file, scale sets how many pixels wide each column is, when nothing else sets the size. svg() uses 10 and bannerSvg() uses 5:
import { bannerSvg, svg } from "ascii.rest/svg";
import { rust } from "ascii.rest/pieces";
svg(rust); // 64 columns: 640 by 640 pixels
svg(rust, { scale: 5 }); // 320 by 320 pixels
bannerSvg("hello"); // 49 columns: 245 by 60 pixels
bannerSvg("hello", { scale: 8 }); // 392 by 96 pixels
taglineSize, spacing, padding and radius are in SVG units. One cell, the space of one character, is 10 units wide and 20 units tall. A letter of the block font is 5 cells tall, so 100 units. A taglineSize of 28 is a little over a quarter of that.
Serve an SVG from your server
Both functions return a string, so any server can send one. Send it with the header Content-Type: image/svg+xml, so browsers treat it as an image.
To make a banner from the text in the URL, see the route handler on next.js. The handler uses only Request and Response, so the same code works in a Cloudflare Worker’s fetch.
When the SVG never changes, make it once, when the server starts, not on every request. This Cloudflare Worker makes the rust logo once and sends the same string to every request:
import { svg } from "ascii.rest/svg";
import * as rust from "ascii.rest/pieces/rust";
const light = svg(rust);
const dark = svg(rust, { dark: true });
export default {
fetch(request: Request) {
const body = new URL(request.url).searchParams.has("dark") ? dark : light;
return new Response(body, { headers: { "Content-Type": "image/svg+xml" } });
},
};
A few things help:
-
Cache it. The same options always make the same SVG, so let browsers and CDNs keep it with a
Cache-Controlheader. -
Compress it. Most of an SVG is frames that look like each other, so gzip makes it far smaller (see keep the file small). Check that your server compresses it: the reply should have a
content-encodingheader.curl -sI -H "Accept-Encoding: gzip" "https://example.com/banner?text=hello" -
Make slow ones ahead of time. Most pieces take under 25 ms to make, but a scene takes 0.2 to 0.5 seconds. The times are in keep the file small.
You may not need a server at all: github readme lists URLs on ascii.rest that make banners and logos for you.
Make SVG files with a script
When the SVG never changes, make it once in a script and commit the file, so you need no server. The script is on github readme, with the <picture> tag that shows a dark file on GitHub’s dark theme.
Keep the file small
An SVG holds every different frame of its loop as text, so it can get big. This table shows how big, and how long each one took to make.
- The sizes were measured from the package as it is built, as text and after
gzip -9. 1 KB is 1,024 bytes. - The times are the middle value of 31 runs, in Node 26 on an Apple M4 laptop.
night-coasthad 3 runs. Your times will differ.
| what | as text | gzipped | time to make |
|---|---|---|---|
svg(rust) |
156.3 KB | 6.3 KB | 5 ms |
svg(archLinux) |
87.2 KB | 3.1 KB | 3 ms |
svg(spinners) |
58.4 KB | 3.4 KB | 2 ms |
svg(donut) |
110.9 KB | 12.5 KB | 25 ms |
svg(donut, { seconds: 2 }) |
56.1 KB | 6.8 KB | 15 ms |
svg(donut, { fps: 8 }) |
59.4 KB | 7.6 KB | 15 ms |
svg(nightCoast) |
9,563.8 KB | 356.3 KB | 290 ms |
bannerSvg("hello") |
90.0 KB | 2.4 KB | 1 ms |
bannerSvg("hello", { effect: "type" }) |
9.3 KB | 0.9 KB | under 1 ms |
bannerSvg("hello", { effect: "still" }) |
3.0 KB | 0.5 KB | under 1 ms |
bannerSvg("ferris", { art: rust, color: "art", tagline: "fast, safe, fun" }) |
261.2 KB | 8.8 KB | 6 ms |
the same ferris banner, with art: { svg: rustSvg } from reuse a logo SVG |
261.2 KB | 8.8 KB | 1 ms |
What that means for you:
- Serve it compressed. gzip made these files between 5 and 38 times smaller. ascii.rest sends
rust.svgas 6,484 bytes gzipped, down from 160,026. - Use a shorter loop or fewer frames. Halving
seconds, or taking 8 frames a second instead of 15, about halves the file. - Let a banner stop.
effect: "type"plays once and stays, so it has few frames.effect: "still"has one. - Keep to small pieces. A scene’s SVG is 5 to 20 MB. Every other piece makes an SVG under 400 KB. SVGs suit logos, banners and small pieces best.
Put two SVGs straight into one HTML page
Usually you show an SVG with <img>, and then you need none of this: each image is its own document.
Every SVG from svg() uses the same CSS class names. Paste two into one HTML page and their styles clash. Give one a prefix with namespaced(), then rebuild it with wrap():
import { namespaced, svg, wrap } from "ascii.rest/svg";
import { donut, rust } from "ascii.rest/pieces";
const first = svg(donut);
const second = wrap(namespaced(svg(rust), "r")!, "rust");
namespaced()returnsnullfor an SVG that ascii.rest/svg didn’t write. That is why the code has!.- For three or more SVGs, give each one but the first its own prefix.
wrap()takes a label for screen readers, then the pixels per column: 10 if you leave it out. Pass 5 for a banner, to keep its size.- This works for any SVG from
svg(), and for a banner with no tagline and no art. A banner with a tagline or art keeps some shared class names, so show it with<img>.
How the SVG works
You don’t need this to use svg(). It explains what is in the file.
svg()plays one loop of the piece and takes a frame 15 times a second, unless you setfps.- It keeps each different frame once, as text. CSS shows the frames in turn.
- There is no script and no font file. The rows use the reader’s own monospace font, stretched so they line up.
- For a reader who has turned on reduced motion, the SVG holds one still frame.
Next
- github readme: ready-made SVG URLs for your README, with no code.
- banners: every banner option, from fonts and shadows to colours and motion.
- next.js: serve a banner SVG from a route handler, or make SVG files when you build.