MCPAI ToolsPlatform EngineeringDocumentation

Beyond Mermaid: PlantUML, D2, and Excalidraw for Technical Diagrams

A practical guide to PlantUML, D2, and Excalidraw — when each tool beats Mermaid, how to control layouts, and how to embed diagrams in your blog with dark/light theme support.

3 April 2026 · 12 min read

In MCP Diagram Tools, we covered draw.io and Mermaid — the two tools that handle most diagramming needs. Mermaid renders inline from markdown, draw.io produces polished architecture diagrams with vendor icons.

But Mermaid has limits. Its auto-layout struggles with complex diagrams that have many edge crossings. It has no real UML compliance — sequence diagrams look right but class diagrams lack proper notation. There is no hand-drawn aesthetic for brainstorming. And the single built-in layout engine (Dagre) offers limited control over node placement.

Three tools fill these gaps: PlantUML for UML-heavy and specialized diagrams, D2 for architecture diagrams with superior layout engines, and Excalidraw for hand-drawn sketches that feel informal and approachable.

At a Glance

CriteriaMermaidPlantUMLD2Excalidraw
RenderingBrowser (JS)Server (Java) or public APICLI (Go binary)Browser (JS editor)
Syntax styleYAML-like keywords@startuml blocksCSS-like nesting {}Visual (not text)
Layout engineDagreGraphvizDagre, ELK, or TALAManual (freeform)
Diagram types~1225+~6Freeform
UML compliancePartialFullNoNo
Theme supportJS themeVariables!theme + skinparam--theme flagStyle presets
Blog embeddingCode block (client-side)Code block (server-rendered)Static SVGStatic SVG
Layout controlSubgraphs, directionPackages, together, skinparamNesting, direction, engine choiceDrag and drop
Best forFlowcharts, sequences in docsUML, C4, network, GanttArchitecture, multi-layer viewsInformal sketches, brainstorms

PlantUML: The UML Powerhouse

PlantUML has been around since 2009 and supports more diagram types than any other text-to-diagram tool. Where Mermaid gives you 12 diagram types, PlantUML gives you 25+.

What PlantUML Adds Over Mermaid

The diagram types Mermaid cannot do at all:

  • Component diagrams — formal UML notation with interfaces and dependencies
  • C4 model diagrams — context, container, component, and code-level views via the C4-PlantUML stdlib
  • Network diagrams (nwdiag) — with proper network/subnet notation and device icons
  • Wireframes (Salt) — UI mockups with forms, tabs, trees, and tables
  • Work Breakdown Structure — hierarchical task decomposition
  • Archimate — enterprise architecture modeling
  • Gantt charts with dependencies — PlantUML Gantt supports task dependencies, milestones, and resource assignment. Mermaid Gantt is display-only.

And the diagram types where PlantUML does it better: sequence diagrams (PlantUML has full UML compliance with activation bars, fragments, and ref/alt/loop blocks), class diagrams (proper UML notation with visibility modifiers, abstract classes, interfaces), and state diagrams (nested composite states, fork/join bars).

Syntax: A Real-Time Data Platform

Here is a component diagram showing the same architecture from the companion post — a real-time data platform with ingestion, stream processing, and multiple storage tiers:

@startuml
left to right direction

package "Ingestion" {
  [WebSocket Connector] as WS
  [REST API Poller] as REST
  [Protocol Adapter] as PROTO
}

package "Message Bus" {
  queue "raw_events" as K1
  queue "filtered_events" as K2
  queue "derived_metrics" as K3
}

package "Stream Processing" {
  [Filter & Normalize] as FILT
  [Enrichment Service] as ENRICH
  [Aggregation Engine] as AGG
  [Anomaly Detection] as ANOMALY
}

package "Storage" {
  database "Redis TimeSeries\n<i>hot, sub-second</i>" as REDIS
  database "TimeSeries DB\n<i>warm, 30d</i>" as TSDB
  database "PostgreSQL\n<i>metadata</i>" as PG
  database "Object Store\n<i>cold archive</i>" as S3
}

package "Serving" {
  [REST API] as API
  [WebSocket Push] as WSOUT
  [Dashboard] as DASH
}

WS --> K1
REST --> K1
PROTO --> K1
K1 --> FILT
FILT --> K2
K2 --> ENRICH
ENRICH --> K3
K3 --> AGG
K3 --> ANOMALY
AGG --> REDIS
AGG --> TSDB
ANOMALY --> PG
ENRICH --> S3
REDIS --> API
REDIS --> WSOUT
TSDB --> API
PG --> API
API --> DASH
WSOUT --> DASH
@enduml

Notice the differences from Mermaid:

  • Typed shapesqueue for Kafka topics, database for storage, [component] for services. Each renders with its proper UML shape (cylinders for databases, rectangles with component notation for services).
  • left to right direction — one line controls the entire flow direction
  • package blocks — group related components with a labeled container, similar to Mermaid’s subgraph but with UML package notation

Layout Control in PlantUML

PlantUML uses Graphviz under the hood, which gives you more layout control than Mermaid’s Dagre engine. Here are the key levers:

Flow direction:

@startuml
left to right direction
' or: top to bottom direction (default)
@enduml

Spacing between nodes:

skinparam nodesep 60
skinparam ranksep 80

nodesep controls horizontal spacing, ranksep controls vertical spacing. Default is 40 for both. Increase these when diagrams feel cramped.

Forcing node alignment with hidden links:

A -[hidden]-> B

This creates an invisible edge that forces Graphviz to place A and B in the same rank (row/column). Useful when two nodes have no logical connection but should be visually aligned.

Grouping with together:

together {
  [Service A]
  [Service B]
  [Service C]
}

Forces nodes into the same cluster regardless of their connections. Unlike package, this has no visual container.

Explicit positioning with skinparam:

skinparam linetype ortho

Forces orthogonal (right-angle) edge routing instead of Graphviz’s default curved lines. Produces cleaner diagrams for architecture views.

Sequence Diagram: Where PlantUML Shines

PlantUML’s sequence diagrams have full UML compliance with features Mermaid lacks. Here is an API authentication flow:

@startuml
participant Client
participant "API Gateway" as GW
participant "Auth Service" as Auth
database "User DB" as DB

Client -> GW: POST /login (credentials)
activate GW
GW -> Auth: validate(credentials)
activate Auth
Auth -> DB: SELECT user WHERE email = ?
activate DB
DB --> Auth: user record
deactivate DB

alt valid credentials
  Auth -> Auth: generate JWT
  Auth --> GW: 200 OK + token
  deactivate Auth
  GW --> Client: 200 OK + token
  deactivate GW

  Client -> GW: GET /data (Bearer token)
  activate GW
  GW -> Auth: verify(token)
  activate Auth
  Auth --> GW: claims
  deactivate Auth
  GW --> Client: 200 OK + data
  deactivate GW

else invalid credentials
  Auth --> GW: 401 Unauthorized
  deactivate Auth
  GW --> Client: 401 Unauthorized
  deactivate GW
end
@enduml

What this has that Mermaid cannot do:

  • Activation bars (activate/deactivate) — show which participant is actively processing at each step
  • alt/else fragments — proper UML combined fragments with labeled guard conditions
  • Return arrows (--> dashed) — distinguish between calls and returns

Embedding and MCP

PlantUML code blocks render the same way as Mermaid on this blog — write ```plantuml in a fenced code block and the client-side renderer converts it to an SVG via the public PlantUML server. Theme-aware skinparam directives are injected automatically. No Java needed locally. For sensitive diagrams, self-host with docker run -d -p 8080:8080 plantuml/plantuml-server:jetty.

MCP server: plantuml-mcp-serverclaude mcp add plantuml -- npx plantuml-mcp-server. Useful for authoring and preview, not needed for rendering.

D2: Modern Architecture Diagrams

D2 is a newer tool (2022, 23k GitHub stars) built specifically for software architecture diagrams. Its syntax is cleaner than PlantUML’s, and it offers something no other text-to-diagram tool has: multiple layout engines including TALA, which produces the best auto-layout for architecture diagrams.

What D2 Adds Over Mermaid

  • TALA layout engine — proprietary but free for personal use. Handles edge crossing, node placement, and spacing better than any other automatic layout engine. Complex diagrams that look tangled in Mermaid come out clean in D2 with TALA.
  • Layers — define multiple abstraction levels in one file. Layer 1 shows high-level architecture, layer 2 adds internal components, layer 3 adds data flows. One source, multiple zoom levels.
  • Scenarios — variant views that inherit from a base. Show the happy path, then the error path, then the degraded path — all from one diagram definition.
  • Steps — animated sequences. Each step builds on the previous, producing a slideshow-style walkthrough.
  • CSS-like nesting — group components naturally with parent { child } syntax instead of separate grouping keywords.
  • Sketch mode — hand-drawn aesthetic (similar to Excalidraw) toggled with --sketch flag:

D2 sketch mode — hand-drawn version of the same pipeline

  • Markdown in labels — rich text formatting inside node labels.

Syntax: The Same Data Platform

Here is the real-time data platform in D2 syntax:

direction: right

ingestion: Ingestion {
  style.fill: transparent
  ws: WebSocket Connector
  rest: REST API Poller
  proto: Protocol Adapter
}

bus: Message Bus {
  style.fill: transparent
  raw: raw_events { shape: queue }
  filtered: filtered_events { shape: queue }
  derived: derived_metrics { shape: queue }
}

processing: Stream Processing {
  style.fill: transparent
  filter: Filter & Normalize
  enrich: Enrichment Service
  agg: Aggregation Engine
  anomaly: Anomaly Detection
}

storage: Storage {
  style.fill: transparent
  redis: |md
    Redis TimeSeries
    *hot, sub-second*
  | { shape: cylinder }
  tsdb: |md
    TimeSeries DB
    *warm, 30d*
  | { shape: cylinder }
  pg: |md
    PostgreSQL
    *metadata*
  | { shape: cylinder }
  s3: |md
    Object Store
    *cold archive*
  | { shape: cylinder }
}

serving: Serving {
  style.fill: transparent
  api: REST API
  wsout: WebSocket Push
  dash: Dashboard
}

ingestion.ws -> bus.raw
ingestion.rest -> bus.raw
ingestion.proto -> bus.raw
bus.raw -> processing.filter
processing.filter -> bus.filtered
bus.filtered -> processing.enrich
processing.enrich -> bus.derived
bus.derived -> processing.agg
bus.derived -> processing.anomaly
processing.agg -> storage.redis
processing.agg -> storage.tsdb
processing.anomaly -> storage.pg
processing.enrich -> storage.s3
storage.redis -> serving.api
storage.redis -> serving.wsout
storage.tsdb -> serving.api
storage.pg -> serving.api
serving.api -> serving.dash
serving.wsout -> serving.dash

Here is the rendered output (generated with d2 --theme=200 for dark mode):

Real-time data platform rendered in D2

Key syntax differences from Mermaid:

  • Nesting with {}ingestion { ws; rest; proto } is natural grouping. In Mermaid you need subgraph blocks with explicit labels.
  • Dotted pathsingestion.ws -> bus.raw references nested nodes. Mermaid requires flat node IDs.
  • Shape declaration inline{ shape: cylinder } on the node itself. No separate style lines.
  • Markdown labels|md ... | blocks allow rich text formatting in any node.

Layout Control in D2

D2 gives you the most layout flexibility of any text-to-diagram tool:

Flow direction:

direction: right
# or: down (default), left, up

Layout engine selection:

d2 --layout=dagre diagram.d2 output.svg   # fast, free, default
d2 --layout=elk diagram.d2 output.svg     # better for dense graphs
d2 --layout=tala diagram.d2 output.svg    # best for architecture

Practical difference: A 15-node architecture diagram with 25 edges will have 3-5 edge crossings in dagre, 1-2 in ELK, and typically 0 in TALA. For anything going into a design document, TALA is worth using.

Nesting for spatial grouping:

vpc: VPC {
  public: Public Subnet {
    alb: ALB
  }
  private: Private Subnet {
    ecs: ECS Service
  }
  public.alb -> private.ecs
}

Containers visually group their children. Unlike Mermaid’s subgraphs, D2 containers can be nested to arbitrary depth and referenced via dotted paths.

Connection routing:

a -> b: {
  style.stroke-dash: 5    # dashed line
  style.stroke: "#D4943A"  # amber color
  style.animated: true      # animated flow
}

Layers: One Diagram, Multiple Views

This is D2’s killer feature for documentation. Define a base diagram and overlay layers for different abstraction levels:

# Base layer — high-level architecture
ingestion: Ingestion Layer
processing: Processing Layer
storage: Storage Layer

ingestion -> processing -> storage

layers: {
  detailed: {
    # This layer inherits the base and adds internals
    ingestion: {
      ws: WebSocket
      rest: REST API
    }
    processing: {
      filter: Filter
      enrich: Enrichment
    }
    storage: {
      redis: Redis
      pg: PostgreSQL
    }
  }
}

Render with d2 --target='' diagram.d2 high-level.svg for the base view and d2 --target='detailed' diagram.d2 detailed.svg for the expanded view. One source file, two outputs, always in sync.

D2 also supports scenarios — variant views that inherit from a base layer. Define the normal flow, then overlay a cache-hit scenario and a database-down scenario. Three views from one file, always in sync. See the D2 docs on scenarios for the full syntax.

Embedding D2 and Getting Started

D2 is a CLI tool — no browser rendering. Export dark and light SVGs with d2 --theme=200 diagram.d2 dark.svg and d2 --theme=0 diagram.d2 light.svg, then embed as images. Install: brew install d2 (macOS) or curl -fsSL https://d2lang.com/install.sh | sh -s -- (Linux). Add --tala for the TALA engine (free for personal use).

MCP servers: d2mcp (10 tools) and claude-d2-diagrams (auto-generates from Terraform/K8s/Docker code).

Excalidraw: The Whiteboard Aesthetic

Excalidraw is not a text-to-diagram language — it is a visual editor that produces hand-drawn-style diagrams using Rough.js. With 120k GitHub stars and adoption by Google Cloud, Meta, and Obsidian, the hand-drawn look is a deliberate design choice, not a limitation.

When to use it: brainstorming sessions, early design phases (signals “this is a draft”), team communication in Slack, and blog posts explaining concepts rather than documenting systems.

Workflow: create at excalidraw.com, use community libraries for AWS/K8s/networking icons, export as SVG or PNG. Excalidraw has a built-in Mermaid parser — paste Mermaid syntax and get a hand-drawn version. Draft in Mermaid for structure, convert in Excalidraw for aesthetic.

Embedding: export dark and light SVG variants, same dual-image CSS pattern as D2. Or export with transparent background — the hand-drawn aesthetic is forgiving about background mismatches.

MCP servers: excalidraw-mcp (official, renders in chat) and @scofieldfree/excalidraw-mcp (opens browser editor with Mermaid conversion).

The Same Diagram, Four Ways

Here is a simplified data pipeline rendered in each tool. Same architecture, different strengths visible:

Mermaid (renders inline)

graph LR
    subgraph Ingestion
        WS["WebSocket"]
        REST["REST API"]
    end
    subgraph Processing
        FILT["Filter"]
        AGG["Aggregate"]
    end
    subgraph Storage
        REDIS["Redis"]
        PG["PostgreSQL"]
    end
    WS --> FILT
    REST --> FILT
    FILT --> AGG
    AGG --> REDIS
    AGG --> PG
    REDIS --> API["API"]
    PG --> API

Clean structure, renders instantly in the browser, source is diffable in git. Limited layout control.

PlantUML (renders inline via public server)

@startuml
left to right direction
skinparam linetype ortho

package "Ingestion" {
  [WebSocket] as WS
  [REST API] as REST
}

package "Processing" {
  [Filter] as FILT
  [Aggregate] as AGG
}

package "Storage" {
  database "Redis" as REDIS
  database "PostgreSQL" as PG
}

[API] as API

WS --> FILT
REST --> FILT
FILT --> AGG
AGG --> REDIS
AGG --> PG
REDIS --> API
PG --> API
@enduml

Proper UML shapes — databases are cylinders, components have the double-rectangle notation. Graphviz layout produces orthogonal edges with skinparam linetype ortho. More verbose syntax, but more precise output.

D2 (syntax shown — renders via CLI)

direction: right

ingestion: Ingestion {
  ws: WebSocket
  rest: REST API
}

processing: Processing {
  filter: Filter
  agg: Aggregate
}

storage: Storage {
  redis: Redis { shape: cylinder }
  pg: PostgreSQL { shape: cylinder }
}

api: API

ingestion.ws -> processing.filter
ingestion.rest -> processing.filter
processing.filter -> processing.agg
processing.agg -> storage.redis
processing.agg -> storage.pg
storage.redis -> api
storage.pg -> api

The cleanest syntax. Nesting groups nodes naturally, dotted paths reference them. TALA layout would produce zero edge crossings. The tradeoff: no browser rendering — you need the CLI. Here is the rendered output:

Simple pipeline rendered in D2

Notice: zero edge crossings, proper cylinder shapes for databases, clean container boundaries. The dagre layout (default, free) produced this — TALA would do even better on more complex diagrams.

Excalidraw (visual editor)

Excalidraw is a visual editor, not a text-to-diagram language — so there is no code block to show here. But it has a powerful bridge: paste the Mermaid syntax from above into excalidraw.com (use the Mermaid import under the hamburger menu), and it converts the structured diagram into hand-drawn style with wobbly lines, imperfect shapes, and a hand-lettered font.

The result signals “this is a concept” rather than “this is the final architecture” — which is often exactly the right tone for a blog post or Slack message.

Choosing the Right Tool

graph TD
    START["Need a diagram"] --> Q1{"Strict UML<br/>compliance?"}
    Q1 -->|Yes| PLANTUML["PlantUML"]
    Q1 -->|No| Q2{"Complex layout with<br/>many edge crossings?"}
    Q2 -->|Yes| D2["D2 with TALA"]
    Q2 -->|No| Q3{"Hand-drawn<br/>aesthetic?"}
    Q3 -->|Yes| EXCALIDRAW["Excalidraw"]
    Q3 -->|No| Q4{"Needs to render<br/>inline in markdown?"}
    Q4 -->|Yes| MERMAID["Mermaid"]
    Q4 -->|No| Q5{"Need vendor icons?<br/>AWS, K8s, etc."}
    Q5 -->|Yes| DRAWIO["draw.io"]
    Q5 -->|No| MERMAID

Quick reference:

  • Mermaid — default choice. Inline rendering, simple syntax, good enough for 80% of cases.
  • PlantUML — when you need UML compliance, C4 diagrams, network diagrams, wireframes, or Gantt charts with dependencies.
  • D2 — when auto-layout quality matters. Complex architecture diagrams with many connections. Multi-layer documentation (layers, scenarios).
  • Excalidraw — when you want the diagram to feel informal. Brainstorming, early design, conversational blog posts.
  • draw.io — when you need vendor icons (AWS, GCP, Azure, K8s) or pixel-level layout control.

Making These Theme-Aware

Each tool handles dark/light themes differently:

ToolTheme MethodApproach
MermaidRuntime (JS)themeVariables in the renderer — adapts automatically when the user toggles themes
PlantUMLRuntime (server)skinparam directives injected before encoding — adapts automatically
D2Build time (CLI)--theme flag — export dark and light SVGs, use CSS to show the right one
ExcalidrawExport timeExport dark and light variants — use CSS to show the right one
draw.ioExport timeExport dark and light SVGs — use the DrawioDiagram component

The rule: if the tool renders at view time (Mermaid, PlantUML), use runtime theme detection. If it renders at build/author time (D2, Excalidraw, draw.io), export both variants and switch with CSS.

For the CSS switching approach, this blog uses a simple pattern:

/* Show dark image by default, swap in light mode */
.drawio-dark { display: block; }
.drawio-light { display: none; }
.light .drawio-dark { display: none; }
.light .drawio-light { display: block; }

This works for any paired dark/light SVGs — D2, Excalidraw, and draw.io all use the same CSS classes and component.

Wrapping Up

Five diagram tools, five different strengths. Mermaid and PlantUML render from code blocks in your markdown — zero export workflow, just write syntax and it appears. D2, Excalidraw, and draw.io produce static images that you export and commit — more manual but more control.

For this blog, the practical stack is:

  • Mermaid for inline flowcharts and sequences (most posts)
  • PlantUML for C4 diagrams and UML-heavy content (renders live, no export needed)
  • D2 for complex architecture diagrams where layout quality matters (CLI export)
  • Excalidraw for informal sketches (visual editor export)
  • draw.io for polished diagrams with vendor icons (editor export)

All five have MCP server integrations for AI-assisted authoring. The companion post covers the MCP workflow in detail — how to prompt for good diagrams, control layout, and iterate. The tools in this post follow the same patterns.

Pick the tool that matches the job. The five minutes of setup for each one pays for itself the first time you need a diagram that Mermaid cannot handle.