Declarative, animated diagrams from YAML — "Mermaid, but sexy." Rendered entirely in C#.
Write a diagram as structured YAML and Beck auto-lays it out, auto-routes the edges, themes it from your site's CSS variables, and animates packets flowing through it. Four diagram types share one document format and one engine — architecture (layered boxes and lines), sequence (lifelines and messages that play the conversation), state (a machine that walks its own transitions), and class (UML cards, generatable from real C# types).
The output is a single self-contained, self-animating inline <svg>: layout, routing, and the full
flow choreography are baked into CSS animations inside the SVG. No client JavaScript, no GSAP, no
runtime, nothing to hydrate — diagrams play even with scripts disabled, respect
prefers-reduced-motion, and adopt the host page's palette and dark mode through CSS variables.
using Beck.Rendering;
string svg = BeckSvg.Render("""
type: architecture
meta: { title: Web Platform, direction: LR }
nodes:
- { id: web, title: Web App, kind: user }
- { id: gw, title: API Gateway, kind: gateway }
- { id: db, title: Postgres, kind: db }
edges:
- { from: web, to: gw }
- { from: gw, to: db, label: reads }
""");Every document opens with a root type: — architecture, sequence, state, or class.
(An untyped document still renders as an architecture diagram, with a deprecation warning.)
type: architecture
meta:
title: Web Platform
direction: TB # TB (default) | BT | LR | RL
nodes:
- { id: web, title: Web App, kind: user }
- { id: gw, title: API Gateway, kind: gateway }
- { id: auth, title: Auth Service }
- { id: authdb, title: Auth DB, kind: db }
groups:
- { id: services, label: Services, members: [auth], accent: primary }
edges:
- { from: web, to: gw }
- { from: gw, to: auth }
- { from: auth, to: authdb, label: reads }Only node ids are required. With no flow: block, a packet animation is auto-derived from the
edges. Validation errors are reported with friendly messages (and a line number for YAML syntax).
The other types swap the middle keys for their own vocabulary:
type: sequence # participants: + messages: (reply/section/activation bars)
type: state # states: + transitions: ("[*]" = entry/exit pseudo-state)
type: class # classes: + relations: (inherits/implements/composition/…)meta—title,subtitle,direction(TB/BT/LR/RL),theme(auto/light/dark),animate(bool),loop(bool),spacing: { rank, node, cornerRadius },narrate(bool to toggle the caption bar, or{ wpm, min, pad }to tune reading-time pacing — seenarratestep).nodes[]—id(required),title,subtitle,icon(named key or raw<svg>),kind(servicedbqueuecachegatewayexternaluserghost),variant(solidsubtleghost),status,accent(tokenprimary|success|warn|danger|info|neutralor a raw color),href/target(renders the card as a link),surface/textColor(per-node color overrides),width,rank,order,group.groups[]—id(required),label,members: [nodeId | groupId…](a member that is a group id nests it — groups compose to arbitrary depth and may span ranks),accent.edges[]—from,to(node or group id),label,style(solid/dashed),curve(step-round/straight/s),kind(data/control/async/dependency),color,arrow(true/false, orend/start/both/none),note(prose that narrates the hop in an auto-derived flow — seenarrate),fromSide/toSide.flow—repeat,repeatDelay,steps[], where each step is one of:packet({from, to, via?, color?, label?}plus look/motion knobsshape(dot/circle/ring),size,speed,glow,impact(burst-on-arrival),ease),burst({from, to|[to…], count, stagger}+ the same knobs),status({node, text, color?}),highlight/pulse({node, color?}),activate/stream({from, to, color?}— persistently recolor an edge / flow continuous dashes along it),working({node, color?}— leave a node breathing untilidle/reset),idle({node}),fail({node, text?, color?}— red shake + flash),narrate("text", or{text, hold?, color?}— set the caption line under the diagram and hold long enough to read it; the hold scales with the text length, tuned bymeta.narrate),phase(label, for seeking),wait(seconds),parallel([steps…]),reset. Setmeta.loop: falseto play the flow once instead of looping.type: sequence—participants[](same fields as nodes) andmessages[](from,to,label,reply,kind,style,color,note(narrates the message while the choreography plays),activate;- section: <label>inserts a band). Request/reply pairs grow activation bars; the derived flow plays the messages in order.type: state— optionalstates[](id,title,subtitle,accent, …) andtransitions[](from,to,label,style,color,note);"[*]"is the entry/exit pseudo-state and undeclared ids auto-create pill states.type: class—classes[](id,name,stereotype,fields[],methods[],accent, …) andrelations[](from,to,kind=inheritsimplementsassociationaggregationcompositiondependency,label,fromCard/toCard), plusgroups[]as namespaces. Being structural reference material, a class diagram renders a still frame — it only animates if you script aflow:.
Beck ships as two NuGet packages:
dotnet add package Beck # the engine + the authoring API
dotnet add package Beck.Skia # optional: exact text measurement with your own font filesRender YAML to SVG. BeckSvg.Render(yaml) returns the <svg> string; write it into any HTML
you produce — a Razor view, a minimal-API endpoint, a static-site build step:
using Beck.Rendering;
string svg = BeckSvg.Render(yaml, new SvgRenderOptions
{
Animation = AnimationMode.Full, // Full (default) | Static | Scrub (scroll-driven)
});Fenced ```beck blocks (the docs-site path). In a Pennington site, a code-block preprocessor
renders every Markdown ```beck fence to inline SVG at build time — the Mermaid-style authoring
experience with zero client cost. See the docs site's BeckSvgPreprocessor for the pattern.
Author YAML from code. The Beck package includes a dependency-free authoring API — one
builder per diagram type (DiagramBuilder, SequenceDiagramBuilder, StateDiagramBuilder,
ClassDiagramBuilder), including ClassDiagramBuilder.FromTypes(...), which reflects real CLR
types into an always-current class diagram:
using Beck;
using Beck.Rendering;
string yaml = new DiagramBuilder("Web Platform")
.Node("web", n => n.Title("Web App").Kind(NodeKind.User))
.Node("api", "API Server")
.Edge("web", "api")
.ToYaml();
string svg = BeckSvg.Render(yaml);Measure with your real fonts. The built-in measurer carries embedded Inter + IBM Plex Mono
metrics, so it works with zero configuration. For pixel-exact card sizing in your own font, add
Beck.Skia and point a SkiaTextMeasurer at the same font files your CSS serves:
using Beck.Rendering;
using Beck.Skia;
using Beck.Rendering.Text;
var font = new BeckFontSpec
{
Family = "IBM Plex Sans",
Files = new Dictionary<int, string> { [400] = "fonts/IBMPlexSans-Regular.ttf", /* … */ },
};
using var measurer = new SkiaTextMeasurer(font);
string svg = BeckSvg.Render(yaml, new SvgRenderOptions { Measurer = measurer, Font = font });Every themeable value is a --beck-* CSS custom property that defaults to the host site's
palette (--color-primary-600, --color-base-*, …) with a literal fallback. The SVG is inline,
so it participates in the page's cascade and matches it automatically, including light/dark —
which is just a matter of [data-theme="dark"] (or prefers-color-scheme) redefining the
variables. There is no per-theme rendering and no hardcoded colors.
A style is a complete visual identity — shapes, strokes, typography, colour bias, and motion
character — chosen with a single token. Beck ships nine. A style only ever redefines the defaults
of the --beck-* tokens, so a themed diagram still adopts your host palette and still flips with
light and dark; the theme axis above is orthogonal and works on top of every style.
| name | look |
|---|---|
classic |
the default card look — soft shadows, rounded corners, Inter + IBM Plex Mono |
minimal |
flat: no shadows, hairline borders, a single travelling dot; glow and rings off |
terminal |
monospace, [bracketed] titles, square packets, green-ramp accent, hard-step trails |
blueprint |
drafting surface — faint grid, dashed edges, dimension ticks on groups, mono uppercase labels |
glow |
gradient edges, soft packet bloom, breathing pulse on active nodes |
brutalist |
thick strokes, a solid blur-free offset shadow, uppercase Archivo, steps() flow motion |
sketch |
hand-drawn — Shantell Sans, deterministically wobbled outlines |
extrude |
2.5D slabs with static depth faces; highlight presses the node down toward its face |
circuit |
chip nodes with pin stubs and a via dot at every route bend |
Set one per document in YAML, or site-wide from C#:
meta: { style: sketch }using Beck;
using Beck.Rendering;
// Site-wide default (a document's own meta.style overrides it):
string svg = BeckSvg.Render(yaml, new SvgRenderOptions { Style = BeckStyles.Glow });Precedence runs meta.style (YAML) → SvgRenderOptions.Style (C# default) → classic — the C#
option is a site-wide default a document opts back out of, deliberately the opposite of Theme.
Every built-in is an instance of the public BeckStyle record; derive your own from the closest one
with a with expression and register it in SvgRenderOptions.Styles. See the style
guide and the custom-style
how-to.
YAML → parse (YamlDotNet) → validate(+defaults, per diagram type) → model
→ measure (ITextMeasurer: embedded Inter metrics, or Skia+HarfBuzz shaping)
→ layout (per type: Sugiyama-lite layered engine, or the fixed sequence grid)
→ route (orthogonal step-round edges with obstacle avoidance / sequence rows)
→ svg (node shapes, group boxes, markers, labels, theming <style>)
→ animate (simulate the flow into an absolute schedule → compile to CSS keyframes)
State and class diagrams compile onto the same layered engine (pills, pseudo-states, and compartment cards are node shapes; UML markers are edge decorations). Sequence diagrams get their own fixed-grid layout and router, but every message is still one continuous SVG path, so the shared packet animation rides it unchanged.
The animation compiler simulates the flow script into an absolute-time schedule, then emits one
shared-duration CSS animation per element with percentage-window keyframes — overshoot and elastic
eases are sampled analytically into CSS linear() timing functions. prefers-reduced-motion (or
animate: false) gets the fully-revealed static frame: all motion CSS lives inside a
@media (prefers-reduced-motion: no-preference) block.
src/Beck— the engine + authoring API (BeckSvg.Render,DiagramBuilderfamily). Only dependency: YamlDotNet.src/Beck.Skia— optional exact text measurement (SkiaSharp + HarfBuzzSharp shaping).src/Beck.Sample— console sample emitting authored YAML.tests/Beck.Tests— xunit suite: model/layout/route golden parity tests, card-sizing gates, render smoke tests.docs/Beck.Docs— the Pennington docs site; every content diagram renders at build time, and the interactive playground runs the same engine compiled to WebAssembly (docs/Beck.Docs.Client).
Beck grew out of abergs/animations by
Anders Åberg — an experiment in animated architecture diagrams with GSAP
and Tailwind CSS. That project was the inspiration and the jumping-off point for this codebase, and
Beck would not exist without it. Thank you, Anders. Beck's first engine was TypeScript + GSAP; the
current engine is a pure-C# port of it, and the CSS choreography is the compiled form of those
original GSAP timelines.