npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bitsnbobs/starch

v2.5.0

Published

Animated diagram library for documenting application internals

Readme

starch

Write text. Get animated diagrams.

Keyframe animation, camera direction, and flex/grid layout for SVG diagrams, in plain text you can version control.

Playground · npm · MkDocs plugin

objects
  client: at 90,120
    clientBg: rect 120x50 radius=10 fill #16202e stroke #22d3ee width=2
    clientLabel: text "Client" size=13 fill #e2e8f0
  api: opacity 0 at 330,120
    apiBg: rect 120x50 radius=10 fill #16202e stroke #34d399 width=2
    apiLabel: text "API" size=13 fill #e2e8f0
  db: opacity 0 at 570,120
    dbBg: rect 120x50 radius=10 fill #16202e stroke #fbbf24 width=2
    dbLabel: text "Database" size=13 fill #e2e8f0
  cache: opacity 0 at 330,270
    cacheBg: rect 120x50 radius=10 fill #16202e stroke #f472b6 width=2
    cacheLabel: text "Cache" size=13 fill #e2e8f0
  req: arrow from=client to=api label="request" color=#7d8590 opacity 0
  q: arrow from=api to=db label="query" color=#7d8590 opacity 0
  hit: arrow from=api to=cache label="hot path" color=#7d8590 opacity 0

animate 7 loop easing=easeInOut
  0.9
    api.opacity: 1
    req.opacity: 1
  1.8
    db.opacity: 1
    q.opacity: 1
  2.7
    cache.opacity: 1
    hit.opacity: 1

Every example below is a playground sample, rendered from the exact text shown.

Shapes, templates, and arrows

Declare objects with ids, position them, connect them. Templates like box, circle, pill, card, and note bundle a background, label, and auto-sizing in one line.

objects
  api: box "API gateway" color=steelblue at 100,70
  worker: circle "Worker" color=mediumseagreen at 340,70
  status: pill "healthy" color=darkorange at 500,70
  doc: note "Plain text in, diagrams out." at 100,210
  info: card "Card" body="With body text" color=mediumpurple at 340,215
  link: arrow from=api to=worker label="jobs" color=steelblue

▶ Open in playground · sample: template-tour

Keyframe animation

An animate block sets property values at points in time; starch interpolates between them. Any property animates — position, color, opacity, layout, camera — and each track can pick its own curve from 17 easings (spring here).

objects
  ingest: card "Ingest" body="raw events" color=steelblue opacity 0 at 40,120
  parse: card "Parse" body="into records" color=mediumseagreen opacity 0 at 40,120
  store: card "Store" body="time series" color=darkorange opacity 0 at 40,120

animate 5 loop
  1
    ingest.opacity: 1
    ingest.transform.x: { value: 130, easing: "spring" }
  2
    parse.opacity: 1
    parse.transform.x: { value: 330, easing: "spring" }
  3
    store.opacity: 1
    store.transform.x: { value: 530, easing: "spring" }

▶ Open in playground · sample: 14-staggered-cards

Camera direction

A camera object frames the scene. Animate look to walk the viewer through a diagram: a target can be an object id, a list (a,b) to fit several, coordinates, or all, and the camera zooms to fit whatever it looks at.

objects
  cam: camera look=all
  a: rect 60x60 radius=6 fill crimson at 50,100
  b: rect 60x60 radius=6 fill limegreen at 300,50
  c: rect 60x60 radius=6 fill royalblue at 550,300

animate 8 loop easing=easeInOut
  2 cam.camera.look: (a)
  4 cam.camera.look: (a,b)
  6 cam.camera.look: (c)
  8 cam.camera.look: all

▶ Open in playground · sample: 20-camera-look-fit

Layout engines

Nest children under a container and give it a layout — flex, grid, or circular. The layout solves positions for you, and layout properties (gap, span, slot, angle) animate like everything else; here the ring advances one slot per second.

objects
  ring: ellipse 220x220 stroke slategray width=1 layout circular radius=110 startAngle=0 at 250,170
    n1: rect 50x30 radius=4 fill steelblue
    n2: rect 50x30 radius=4 fill coral
    n3: rect 50x30 radius=4 fill seagreen
    n4: rect 50x30 radius=4 fill gold
    n5: rect 50x30 radius=4 fill mediumpurple
    n6: rect 50x30 radius=4 fill tomato

animate 6 loop easing=easeInOut
  1 ring.layout.startAngle: 60
  2 ring.layout.startAngle: 120
  3 ring.layout.startAngle: 180
  4 ring.layout.startAngle: 240
  5 ring.layout.startAngle: 300
  6 ring.layout.startAngle: 360

▶ Open in playground · sample: 17-layout-circular

Live editor

The playground is where you write these: type on the left, the diagram redraws on the right as you go, with playback and chapter controls underneath. Errors show up as you type without blanking the last good frame.

Its sample browser is also the way in. The numbered samples are a course: 22 lessons that build one diagram a step at a time, from a single rectangle through templates, connections, animation, layout and camera — each one introducing a single new idea, so you can read a lesson as a diff against the one before it. The unnumbered samples are reference grids: every shape in a set side by side, all 17 easings racing, every colour format.

Diagrams embedded in your own pages can open it too: add editable to a <starch-diagram> and its edit button round-trips the DSL back into the page.

Quick start

A script tag, the same pattern as mermaid. It registers the <starch-diagram> element and exposes a Starch global:

<script src="https://unpkg.com/@bitsnbobs/starch/dist/starch-embed.iife.js"></script>

<starch-diagram autoplay>
  a: box "It works" color=steelblue at 100,100
</starch-diagram>

Or install it (ESM-only, no dependencies):

npm install @bitsnbobs/starch
import { StarchDiagram } from '@bitsnbobs/starch';

const diagram = new StarchDiagram(container, { dsl, autoplay: true });

Run the playground locally

No clone needed:

npx @bitsnbobs/starch          # one-off

npm install -g @bitsnbobs/starch
starch                         # or install the `starch` command

Both serve it at http://localhost:4600 and open a browser (--port <n>, --no-open).

Checking a document

starch check parses a document and reports anything it had to drop:

starch check diagram.starch        # a file, or several
cat diagram.starch | starch check -    # or stdin
starch check diagram.starch --json     # machine-readable

It exits non-zero when a document fails to parse or produces a warning. Warnings matter as much as errors here: the parser drops what it can't match rather than failing, so a warning means the diagram that renders is not the one that was written — a misspelled shape or property name shows up as Node "api" has no properties.

The same check is available programmatically as parseScene, which needs no DOM — use that to validate starch inside your own app, or to expose a checking tool to whatever is generating the diagrams.

The other half of that loop is starch grammar, which prints the DSL — generated from the schemas, so it matches the version you have — for a model that has to write starch without having it installed. See getStarchGuide.

To work on starch itself:

git clone https://github.com/zummed/starch.git
cd starch && npm install
npm run dev                    # playground dev server on :5174

How the DSL is structured

A scene is plain text with meaningful indentation, in up to four parts — directives, styles, objects, and one animate block:

name "My scene"            // directives: name, background, viewport
background #14161c
viewport 800x400

style hot                  // reusable styles, applied with @hot
  fill crimson

objects
  a: rect 120x50 radius=8 @hot at 100,100
    aLabel: text "A" size=13 fill white
  b: box "B" color=steelblue at 300,100
  ab: arrow from=a to=b label="to B" color=gray

animate 4 loop
  2 a.opacity: 0.3
  4 a.opacity: 1

Objects and ids. Every object is id: type properties.... Ids must be globally unique — including nested children — because they're how arrows, animations, and the camera refer to things. Indenting an object under another makes it a child: children move with their parent and inherit fill and opacity.

Primitives and templates. The primitives are rect, ellipse, text, path, and camera. On top of them, templates auto-size around their label: box, circle, pill, card, note, group, textblock, codeblock, table — and a state-machine set (state.node, state.initial, state.final, state.choice, state.region, enabled with use [core, state]). Both color and colour are accepted everywhere. A shape's properties can go anywhere on its line, on either side of at x,y; the names the object itself owns (at, dash, layout, opacity, depth, visible, and style as the written-out form of @name) always set the object rather than the shape. fill and stroke go by spelling: bare (fill steelblue) they are the object's and children inherit them, with an = (box "X" fill=azure) they set that shape's own.

Shapes whose content is a list take it from the indented block beneath the line — one quoted string per line for textblock and codeblock, one row of quoted cells for table. Quoting is what lets a line hold //, =, brackets or leading indentation without any of it being read as syntax.

notes: textblock size=13 at 40,60
  "One quoted string per line."
  "Punctuation, = signs and // all survive."

t: table cols=["Shape", "Content"] colWidth=110 at 300,60
  "textblock" "lines"
  "table" "rows"

Arrows and lines. arrow from=a to=b label="..." color=... connects object edges (not centers) with an arrowhead; line is the same without the head. The a -> b form takes waypoints and curve options: a -> (250,100) -> b radius=15 routes a polyline, a -> b bend=1 bends smoothly, and smooth fits a spline through waypoints. A label rides the midpoint of the route and stays legible over whatever it crosses: by default a soft halo hugs the glyphs, labelBg=plate sets it on an opaque slab instead, and labelBg=none leaves it bare. labelMaxWidth=120 wraps a long label onto more lines rather than letting it run the length of the connection.

Animation. animate <duration> [loop] [easing=...] opens the timeline. Each keyframe is a time — absolute (2), or relative to the previous one (+1), optionally with delay=0.5 — followed by target.property: value lines. Values hold between keyframes, and dot-paths reach anything: row.layout.gap, cam.camera.zoom, and the parts inside a shape — a card named c exposes c.bg, c.header, c.divider and c.body, so c.header.fill animates its header. parseScene(dsl).trackPaths lists every path a document offers. A value can also be { value: 480, easing: "bounce" } to ease one track differently. Easings: linear, easeIn/Out/InOut, easeIn/Out/InOutCubic, easeIn/Out/InOutQuart, easeIn/OutBack, bounce, elastic, spring, snap, step.

Camera. cam: camera look=(300,170) zoom=1.5 ratio=1.78look accepts a point, an object id (the camera follows it), (id,dx,dy) for an offset, a list (a,b) to fit several objects, or all. Without a camera, the view auto-fits the scene. Multiple cameras can hand off with active, and cam.transform.rotation rotates the view.

Chapters. Named time markers turn an animation into a step-through presentation (the element's controls and the API expose next/previous):

a: rect 100x50 fill steelblue at 100,100

animate 10
  chapters
    chapter "Start" at 0
    chapter "Handshake" at 3
    chapter "Complete" at 7

Embedding

The package ships four entry points:

| Import | What it gives you | | --- | --- | | @bitsnbobs/starch | Core: StarchDiagram, renderToSVG, edit-link helpers, parser/renderer building blocks. Framework-free. | | @bitsnbobs/starch/react | useV2Diagram hook. Requires React (optional peer dependency). | | @bitsnbobs/starch/element | Registers the <starch-diagram> custom element (for apps with a bundler). | | @bitsnbobs/starch/embed | Self-contained IIFE for <script> tags — registers the element and exposes the Starch global. |

The <starch-diagram> element

<starch-diagram autoplay speed="1.5">...DSL...</starch-diagram>
<starch-diagram src="/diagrams/arch.starch" autoplay></starch-diagram>

Hover shows playback controls; parse and fetch errors render as an overlay. From JS the element is a full player:

const el = document.querySelector('starch-diagram');
el.dsl = newDsl;                       // live-update
el.play(); el.pause(); el.seek(2.5); el.goToChapter('step-2');
el.addEventListener('starch:chapterenter', (e) => { /* e.detail */ });
el.addEventListener('starch:error', (e) => { /* e.detail.message */ });

Markdown code blocks

Starch.scan() turns any <code class="language-starch"> block (or div.starch) in rendered HTML into a live diagram, the same way mermaid does:

Starch.scan();            // whole document
Starch.scan(container);   // or a subtree

For MkDocs there's a ready-made plugin:

pip install mkdocs-starch
# mkdocs.yml
plugins:
  - starch

See mkdocs-plugin/README.md for options.

React

import { useV2Diagram } from '@bitsnbobs/starch/react';

function App() {
  const diagram = useV2Diagram({ dsl, autoplay: true });
  return <div ref={diagram.containerRef} style={{ width: '100%', height: 400 }} />;
}

Static SVG

No animation loop, no mounted component — for thumbnails, previews, or export:

import { renderToSVG } from '@bitsnbobs/starch';

const svg = renderToSVG(dsl);              // final frame
const svgAtStart = renderToSVG(dsl, { time: 0 });

renderToSVG needs a DOM (browser, or happy-dom/jsdom in Node) — the README images above are generated exactly this way (docs/readme/build.sh).

It throws on malformed DSL, but it does not report warnings: a document with a misspelled shape name renders successfully, just missing that shape. A clean render is therefore not proof the diagram is right — check with parseScene or starch check when the DSL was generated rather than hand-written.

Errors

setDSL returns a result instead of throwing, so a live editor can keep the last good frame on screen while the user types:

const result = diagram.setDSL(newDsl);
// { ok: true, warnings: string[] } | { ok: false, error: string }

diagram.on('error', (e) => console.log(e.message));
diagram.error;     // last parse error, or null
diagram.warnings;  // warnings from the last successful parse

Editing embedded diagrams

Embedded diagrams are players; editing happens in the playground, wired up three ways:

1. The editable attribute adds an edit button to the element's controls. It opens the playground in a popup with the current DSL; when the user saves, the diagram updates in place and fires starch:edit so your app can persist the new text:

<starch-diagram editable autoplay>...DSL...</starch-diagram>
el.addEventListener('starch:edit', (e) => save(e.detail.dsl));

Point edit-url="https://your-fork.example/" at your own playground deployment if you have one.

2. Edit links. For a plain "open in playground" link (like the ones under the examples above), put the DSL in the URL fragment — it never leaves the browser:

import { buildEditUrl } from '@bitsnbobs/starch';

const url = buildEditUrl(dsl);                               // playground in embed mode
const full = buildEditUrl(dsl, undefined, { embed: false }); // full playground, DSL imported as a tab

3. Host your own editing session. Open the playground yourself — iframe modal or popup — and listen for postMessage. The playground posts { source: 'starch-playground', type: 'ready' | 'save' | 'cancel', dsl? } to its opener/parent; type guards ship in the package:

import { buildEditUrl, isPlaygroundMessage } from '@bitsnbobs/starch';

const frame = document.createElement('iframe');
frame.src = buildEditUrl(currentDsl);
window.addEventListener('message', (e) => {
  if (e.source !== frame.contentWindow || !isPlaygroundMessage(e.data)) return;
  if (e.data.type === 'save') persist(e.data.dsl);
  if (e.data.type === 'save' || e.data.type === 'cancel') closeModal();
});

API reference

StarchDiagram

const diagram = new StarchDiagram(container, { dsl, autoplay: true, speed: 1, onEvent });

diagram.play();  diagram.pause();  diagram.seek(2.5);  diagram.setSpeed(2);
diagram.nextChapter();  diagram.prevChapter();  diagram.goToChapter('name');
diagram.on('chapterEnter', handler);  diagram.off('chapterEnter', handler);
diagram.destroy();

diagram.time;  diagram.duration;  diagram.playing;  diagram.speed;
diagram.chapters;  diagram.activeChapter;

Events: chapterEnter, chapterExit, ended, error.

<starch-diagram> attributes

src, autoplay, speed, editable, edit-url.

Starch global (embed script only)

Starch.scan(root?, { autoplay });      // upgrade language-starch code blocks in place
Starch.render(container, dsl, opts);   // → StarchDiagram
Starch.renderToSVG(dsl, { time });     // → SVG string
Starch.buildEditUrl(dsl);              // → playground edit link
Starch.StarchDiagram;                  // the classes themselves
Starch.StarchDiagramElement;

parseScene — checking and inspecting a document

parseScene(dsl) is the programmatic form of starch check. It needs no DOM, so it runs anywhere — a build step, a CI job, a server, or a tool your own app exposes to an AI agent:

import { parseScene } from '@bitsnbobs/starch';

const scene = parseScene(dsl);

scene.warnings;    // string[] — empty means the diagram matches what was written
scene.trackPaths;  // string[] — every dot-path this document can animate
scene.nodes;       // the resolved node tree
scene.animate;     // the parsed timeline, if the document has one
scene.name;        // and description, background, viewport, images, use

Two fields matter most when generating starch programmatically:

warnings is the correctness signal. The parser drops what it can't match rather than throwing, so a non-empty warnings means the rendered diagram is not the one that was written — a misspelled shape or property shows up as Node "api" has no properties, and a document with nothing recognisable in it reports zero nodes. Treat a warning as a failure. Malformed structure (a duplicate id, two geometry fields on one node) still throws, so wrap the call:

function check(dsl) {
  try {
    const { warnings } = parseScene(dsl);
    return { ok: warnings.length === 0, errors: [], warnings };
  } catch (err) {
    return { ok: false, errors: [err.message], warnings: [] };
  }
}

trackPaths answers "what can I animate here?" — it lists every path the document exposes, including the parts inside shapes (c1.bg.fill, c1.label.fill for a box named c1), so you never have to guess a dot-path.

To go all the way to an image, renderToSVG(dsl, { time }) returns an SVG string — but unlike parseScene it requires a DOM, and it renders a document with warnings without complaint, so check first.

getStarchGuide — handing the DSL to whatever writes it

Starch is often written by a model rather than a person, and that model usually doesn't have starch installed — an app that stores .starch documents is asked to produce one by an agent that has never seen the syntax. getStarchGuide() returns that syntax as a markdown string, so the app can forward it from its own tool without depending on anything in here:

import { getStarchGuide } from '@bitsnbobs/starch';

getStarchGuide();                    // ~11 KB of markdown, every registered set
getStarchGuide({ sets: ['core'] });  // just the shapes you let documents use
getStarchGuide({ examples: false }); // drop the worked examples

It needs no DOM and does no parsing. The same text is available without installing the package at all:

starch grammar > STARCH.md
starch grammar --set core

It is generated, not written. The shape inventory comes from the template registry, property names and their descriptions from the schemas' .describe(), each shape's writing form from the same dsl() hints the parser reads, the easing list from EasingNameSchema, the strategy tables from LAYOUT_STRATEGY_SCHEMAS. Adding a property to a schema adds it to the guide; renaming one stops the old name being taught. Only the invariants no schema encodes — that ids are globally unique, that indentation nests, how keyframe times read — are prose. A test parses every example in the guide and fails on a single warning, so it can't teach syntax that doesn't work.

Pair it with parseScene: forward the guide so the model knows what to write, then check what comes back and hand the warnings straight to it.

Lower-level exports

The core entry also exports the pipeline pieces (buildTimeline, evaluateAllTracks, applyTrackValues, emitFrame, SvgRenderBackend, computeViewBox, computeAutoFitViewBox, layout and text-measurement utilities) for building custom renderers or tooling on top.

Development

npm run dev          # playground dev server
npm test             # run tests
npm run build        # library entries + types
npm run build:embed  # standalone embed (IIFE)
npm run build:app    # playground (also what `npx @bitsnbobs/starch` serves)
npm run build:all    # everything
docs/readme/build.sh # regenerate the README images (needs inkscape, ImageMagick, chrome)

Releases are automated: merging to main runs semantic-release, which versions and publishes from the commit messages.

License

ISC