altsvg
v1.0.0
Published
Turn alt text into a same-size SVG placeholder: wrapped, fitted and accessible. For broken images, lite modes, offline pages and mockups.
Downloads
40
Maintainers
Readme
+--[ altsvg ]--------------------------------------------+
| |
| |
| Mountains at sunrise over a still lake |
| |
| |
+------------------------------------------- 400x250 ----+npm i altsvgimport { generateAltSVG } from "altsvg";
generateAltSVG(400, 250, "Mountains at sunrise over a still lake");
// <svg xmlns="http://www.w3.org/2000/svg" width="400" height="250" viewBox="0 0 400 250" role="img" ...>Why
When an image is missing, pages break in quiet ways. Thumbnails collapse, captions jump up, overlays land on top of the text, and the reader has no idea what was there. altsvg puts an SVG of the same size in the image’s place, showing its alt text. The layout holds and the meaning survives.
Common uses:
- Broken images. Swap any image that 404s, times out or is blocked for its alt text, in one line.
- Lite and data-saver modes. Skip image downloads and keep a page that reads well and doesn’t shift as it loads.
- Offline copies and archives. Strip heavy media from saved pages and documentation bundles without wrecking the layout.
- Mockups and tests. Exact-size placeholders that say what goes there, generated locally with no image service.
- Alt text review. Render a page with every image replaced by its alt text, so file names, empty alts and vague descriptions stand out.
- Static sites and pipelines. Rewrite generated HTML at build time with the CLI or one function call.
Features
- Two styles.
plaindraws clean, fitted sans-serif text.asciidraws a printed, terminal-style card: a+--[ alt ]--+frame in monospace, which also comes out as plain text. - Fits the text to the box. Text is wrapped using per-character font metrics. If it still doesn’t fit, the font shrinks toward a minimum size, and only then is the last line truncated with an ellipsis. Text never spills outside the image.
- Same size, scales with CSS. Width and height match the image, and a
viewBoxkeeps the text in proportion undermax-width: 100%. - Accessible. Every SVG has
role="img", anaria-labeland a<title>. - Safe with any input. Text is XML-escaped exactly once, and characters that would break XML are removed.
- Any script. Long words, URLs and text without spaces (such as CJK) are split by grapheme, so emoji and accented letters stay intact.
- Tiny images handled. Boxes too small for legible text get a clean frame, with the text kept in
<title>. - Runs anywhere. The output is a plain string, so it works in Node, workers, edge runtimes and browsers. There are zero dependencies, the browser bundle is 4.7 kB gzipped, and TypeScript types are included.
Install
npm i altsvgOr load it from a CDN, which exposes everything on the AltSVG global:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/altsvg.min.js"></script>Requires Node.js 18.3 or later. Works with import and require.
Usage
Generate a placeholder
import { generateAltSVG, generateAltSVGDataURI } from "altsvg";
// SVG markup
const svg = generateAltSVG(400, 250, "Mountains at sunrise over a still lake");
// A compact data URI, for <img src> or CSS backgrounds
const src = generateAltSVGDataURI(400, 250, "Mountains at sunrise over a still lake", {
background: "none",
borderColor: null,
});The ASCII style
import { generateAltSVG, generateAltASCII } from "altsvg";
// A printed, terminal-style card as SVG, at exactly 400 × 250
const card = generateAltSVG(400, 250, "Mountains at sunrise over a still lake", {
style: "ascii",
label: "hero.jpg", // tag in the top border (default "alt", null for none)
});
// The same card as plain text, for terminals, READMEs, logs or print
console.log(generateAltASCII(400, 100, "Mountains at sunrise over a still lake"));+-[ alt ]---------------------------------------------+
| |
| Mountains at sunrise over a still lake |
| |
| |
+------------------------------------------ 400x100 --+The frame, label and pixel size fill the image box. Each run of characters is pinned to its grid cell, so the card lines up in any monospace font. style: "ascii" works everywhere options are accepted, including installAltFallback and replaceImagesWithPlaceholders. Boxes too small for a readable grid fall back to the plain style.
Replace broken images in the browser
import { installAltFallback } from "altsvg";
// Replaces images that have already failed, and any that fail later,
// including images added to the page after this call.
const stop = installAltFallback(document);
// Stop watching when you no longer need it
stop();With no bundler:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/altsvg.min.js"></script>
<script>
AltSVG.installAltFallback();
</script>The size comes from the image’s width and height attributes, then its rendered size, then defaultWidth × defaultHeight (300 × 150). To replace a single image immediately, call replaceImage(img).
Rewrite an HTML string
import { replaceImagesWithPlaceholders } from "altsvg";
const lite = replaceImagesWithPlaceholders(html, {
filter: (img) => Number(img.width) >= 48, // keep small icons as real images
svg: { background: "#f4f4f5" },
});For each <img> with numeric width and height, replaceImagesWithPlaceholders:
- sets
srcto a placeholder data URI; - removes
srcset,sizes,data-srcanddata-srcset, so the original is never requested; - adds the class
altsvg-placeholder; - keeps every other attribute.
Images without a usable size are left untouched.
Command line
# One placeholder
npx altsvg 400 250 "Mountains at sunrise" -o placeholder.svg
# As a data URI
npx altsvg 120 80 "A red fox" --data-uri
# The printed ASCII card, as SVG or as text in your terminal
npx altsvg 400 250 "Mountains at sunrise" --ascii -o card.svg
npx altsvg 400 250 "Mountains at sunrise" --text --label hero.jpg
# Rewrite every sized <img> in a file (or stdin with -)
npx altsvg --html page.html -o page.lite.html
cat page.html | npx altsvg --html - --no-border --background none| Flag | Effect |
| ----------------------- | ------------------------------------------ |
| -o, --out <file> | Write to a file instead of stdout |
| --data-uri | Print a data: URI instead of raw SVG |
| --html <file\|-> | Rewrite <img> tags in an HTML file |
| --ascii | Use the printed ASCII style |
| --text | Print the ASCII card as plain text |
| --label <text> | ASCII: tag in the top border |
| --no-label | ASCII: no tag in the top border |
| --no-size | ASCII: no pixel size in the bottom border |
| --font-size <px> | Preferred font size (default 14) |
| --min-font-size <px> | Smallest size before truncating (default 9)|
| --padding <px> | Inner padding (default 8) |
| --background <color> | Background fill, or none |
| --text-color <color> | Text colour |
| --border <color> | Border colour |
| --no-border | Draw no border |
API
Functions
| Function | Returns | Description |
| ------------------------------------------------ | ------------ | --------------------------------------------------------------------------- |
| generateAltSVG(width, height, alt, options?) | string | SVG markup of exactly width × height px. Throws RangeError on a non-positive or non-finite size. Empty alt text shows fallbackText. |
| generateAltSVGDataURI(width, height, alt, options?) | string | The same SVG as a URL-encoded data:image/svg+xml URI. |
| generateAltASCII(width, height, alt, options?) | string | The ASCII-style card as plain text, matching what style: "ascii" draws at that pixel size. Empty string when the box is too small. |
| installAltFallback(root?, options?) | () => void | Browser only. Replaces images under root (default document) that fail to load, now or later. Returns a function that stops watching. |
| replaceImage(img, options?) | void | Browser only. Replaces one image right away. Calling it twice is safe. |
| replaceImagesWithPlaceholders(html, options?) | string | Rewrites <img> tags in an HTML string. Options: svg, filter(attrs), className. |
| svgToDataURI(svg) | string | Encodes any SVG string as a data URI. |
| layoutText(text, innerWidth, innerHeight, opts)| TextLayout | Runs only the fitting step and returns { lines, fontSize, lineHeight, truncated }. |
Also exported: layoutAscii, wrapText, ellipsize, measureText, measureCells, charWidth, escapeXML and normalizeAltText.
Options
| Option | Default | Description |
| --------------- | ------------------------------------------------- | -------------------------------------------------------- |
| style | "plain" | "ascii" for the printed, monospace card |
| fontSize | 14 (12 for ASCII) | Preferred font size in px |
| minFontSize | 9 | Smallest font size tried before truncating |
| lineHeight | 1.3 | Line height as a multiple of the font size |
| padding | 8 | Inner padding, capped at 10% of the shorter side |
| fontFamily | Arial, Helvetica, "Liberation Sans", sans-serif | Font stack. The default matches the built-in metrics |
| background | #f8f9fa | Background fill, or "none" for transparent |
| textColor | #54595d | Text colour |
| borderColor | #c8ccd1 | 1px border, or the frame characters in the ASCII style. null for none |
| label | "alt" | ASCII only: tag in the top border, or null |
| showSize | true | ASCII only: pixel size in the bottom border |
| fallbackText | "Image unavailable" | Text used when the alt text is empty |
| defaultWidth | 300 | Browser helpers only: width for images with no size |
| defaultHeight | 150 | Browser helpers only: height for images with no size |
How text fitting works
- Wrap. Break lines between words at the preferred font size. A word wider than the box is split by grapheme.
- Shrink. If the block is too tall, reduce the font size one pixel at a time, down to
minFontSize. - Truncate. If it still doesn’t fit, keep the lines that fit and end the last one with
…. If fewer than three characters would be visible, draw an empty frame instead.
Widths are estimated from Helvetica metrics, which Arial and Liberation Sans share, with a small safety margin. This is why the default font stack lists those fonts first. Other fonts work too, but the text may wrap slightly earlier or later than it would with those metrics.
Limitations
- Text width is estimated, not measured by a browser. Scripts without a metrics table use a conservative average width.
- Line breaking follows simple rules (spaces, then graphemes), not the full Unicode line-breaking algorithm.
replaceImagesWithPlaceholdersuses a tolerant regex-based tag parser. It handles real-world<img>markup well but is not a full HTML parser.
Development
npm install
npm test # node:test suites in test/
npm run typecheck # type-check sources and tests
npm run build # dist/ (CommonJS + types), the browser bundle, and site/altsvg.min.jsThe landing page lives in site/. It is a static page that runs the real browser bundle, so after npm run build you can serve site/ from any static host, such as GitHub Pages, Netlify or Vercel.
src/
index.ts Public API
svg.ts generateAltSVG, generateAltASCII, layoutText, svgToDataURI
ascii.ts The ASCII style: grid layout and SVG rendering
browser.ts installAltFallback, replaceImage
html.ts replaceImagesWithPlaceholders
wrapText.ts Word wrapping, grapheme breaks, ellipsis
measure.ts Text width estimation
escape.ts XML escaping
cli.ts The altsvg command
test/ Tests
site/ Landing pageLicense
ISC
