D2 turns a text description of a system into a diagram. You write what exists and how it connects; a layout engine decides where everything goes. This page is the first stop before you write one: the mental model, the syntax you will use every day, how layout works, and recipes you can copy.
user: Customer {shape:person}
shop: Online shop {
web: Web app
api: API
web -> api: JSON
}
db: Orders DB {shape:cylinder}
user -> shop.web: browses
shop.api -> db: writes
Start in two minutes
You picked the CLI in watch mode, which is the most comfortable loop: you save the file, the browser tab refreshes with the new render.
Install
The install script picks the best method for your machine (it uses your package manager if it can). Run it with --dry-run first to see what it will do.
$ curl -fsSL https://d2lang.com/install.sh | sh -s -- --dry-run
$ curl -fsSL https://d2lang.com/install.sh | sh -s --
$ d2 --version
Alternatives: go install github.com/d2lang/d2@latest, or a binary from GitHub releases. Since 0.9.0 the TALA engine and PNG/PDF/PPTX/GIF export are built in, so there is nothing else to install.
A browser tab opens and live-reloads on every save. Errors show up in that tab and in the terminal, with line numbers.
Get editor support
Install the VS Code extension or the Vim plugin. Syntax highlighting catches many errors as you type: if a word suddenly changes color, it probably hit a reserved symbol.
fmt fixes indentation and shortens imports. The output format follows the file extension (svg, png, pdf, pptx, gif, or txt for ASCII art). The last line shows how to switch layout engine for a session.
The mental model
D2 has very little syntax because almost everything is the same idea: a key, optionally a label, optionally a map of fields in braces. Read these six ideas once and most of the language becomes predictable.
aws.dbkey path:Orders DBlabel{shape: cylinder}map of fields
websource key->arrowaws.dbtarget key: readslabel{style.stroke-dash: 3}map of fields
Keys are identity, labels are display
The key is the name you use in code; the label is what the reader sees. If you give no label, the key is shown. Keep keys short (be, db) and labels readable (Backend). Keys are case-insensitive.
# Key on the left, label on the right
be: Backend
fe: Frontend
# Connections use KEYS, not labels
fe -> be: calls
BackendFrontendcalls
Connections point at keys, and mentioning a key creates it
There is no separate "declare" step: a -> b creates both shapes if they do not exist. The flip side is the most common beginner bug: writing a label in a connection (Backend -> Frontend) silently creates two new shapes.
Same key, same object: declarations merge
You can declare a shape once and add fields to it anywhere later. Each line refines the same object. Connections are the exception: writing a -> b twice draws two arrows.
# Declared once...
api: Payments API
# ...then extended anywhere later.# Same key = same object, settings merge.
api.shape:hexagon
api.style.fill:"#FFF4D6"
api -> ledger
Payments APIledger
Nesting is scope
A shape with a map of children becomes a container. Dots build a path into it (aws.vpc.app), and _ means "my parent" when you need to reach outward from inside a container.
aws: AWS {
vpc: VPC {
app: App server
}}# Dots form a path to a nested key
aws.vpc.app -> aws.s3: uploads
AWSVPCs3App serveruploads
You describe, the engine places
There are no coordinates, and the order of lines usually does not matter (sequence diagrams are the exception). You steer the result with a few levers instead: direction, the choice of layout engine, grids, and near. See Layout.
Keep the system apart from its look
D2's own design principle is "design the system, not the diagram". Describe shapes and connections first, then style them in bulk with classes, globs, variables and themes. Styles can even live in a separate file you import.
Syntax basics
Each card shows the source on the left and what D2 0.9.0 actually rendered on the right. Every snippet opens in the Playground so you can tinker.
No card matches. Try a broader word, or search the official docs.
A key on its own creates a rectangle. key: label sets the text, and shape picks another form. Semicolons put several declarations on one line. All 18 standard shapes are in the catalog below.
# A bare key creates a rectangle
server
# key: label
db: PostgreSQL
db.shape:cylinder# Several shapes on one line
cache; jobs
jobs.shape:queue
cache.shape:stored_data
Set source-arrowhead / target-arrowhead on a connection. Options: triangle (default), arrow, diamond, circle, box, cross, and crow's feet cf-one, cf-one-required, cf-many, cf-many-required. An arrowhead only appears on an end that has one, so a source arrowhead needs <->. Repeated connections are separate objects you can target by index.
direction:right# A source arrowhead needs a source end: use <->
users <-> orders: places {source-arrowhead.shape:cf-one-requiredtarget-arrowhead.shape:cf-many}# Same pair twice = two connections
a -> b
a -> b
# Target one of them by index (0-based)(a -> b)[1].style.stroke: red
Give a shape a map with children and it becomes a container. Label it with the shorthand aws: AWS {…} or with label: inside. Use dot paths from outside and _ to reach the parent from inside.
Every visual property lives under style, either as a map or with dot notation (workers.style.multiple: true). Connections take styles too. Quote hex colors, because # starts a comment.
direction:right
api:{style:{fill:"#E8EEFF"stroke:"#0D32B2"stroke-width:2border-radius:8shadow:true}}
workers.style.multiple:true
api -> workers: jobs {style.stroke-dash:3style.animated:true}
Block strings open with a pipe and a language: |md for Markdown, |go, |ts… for highlighted code, |latex for math. They close with a matching |. If the content itself contains a pipe, use ||md … ||. shape: text gives plain floating text.
why:|md ### Why a queue? - absorbs traffic bursts - **retries** come for free|
snippet:|go q.Publish(ctx, job)|
hint: just floating text {shape:text}
why -> snippet
Why a queue?absorbs traffic burstsretries come for freeq.Publish(ctx,job)q.Publish(ctx,job)just floating text
icon takes a URL or a local path; shape: image shows the icon alone. Browse the free set at icons.d2lang.com. tooltip and link make SVG output interactive (in PNG/PDF they move to an appendix).
near pins a shape to the edge of the diagram: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right. On label and icon it also accepts outside-… and border-… variants. That is how you add titles and side notes.
A class is a named bundle of fields (shape, style, size, even label). Apply it with class:, or several at once with class: [a; b]. Fields you set directly on the shape win over the class.
classes:{
service:{style.fill:"#E8EEFF"style.border-radius:6}
store:{shape:cylinderstyle.fill:"#FFF4D6"}}
api.class: service
auth.class: service
db.class: store
api -> db
auth -> db
vars defines values you reuse with ${name}. The special d2-config map holds settings that would otherwise be CLI flags (layout-engine, theme-id, dark-theme-id, pad, center, sketch), so the file renders the same everywhere. Flags and environment variables still take precedence.
Globs change many things in one line. * matches siblings in the current scope, ** recurses into containers, *** also reaches layers and imports. (* -> *)[*] targets connections. Filter with &field: value (or !& to exclude). Globs also apply to shapes declared after them.
web -> api -> db
api -> cache
db.shape:cylinder
cache.shape:cylinder# Every top-level shape*.style.border-radius:8# Only shapes that are cylinders (filter with &)*:{&shape:cylinderstyle.fill:"#FFF4D6"}# Every connection(*->*)[*].style.stroke-dash:3
# comments to the end of the line; """ opens and closes a block comment. Labels are usually unquoted. Quote them when they contain symbols D2 uses (: ; # { } . $). Single quotes also stop ${…} substitution.
# Line comment
x -> y # trailing comment"""Block comment,spanning lines"""# Quote labels that contain reserved symbols.# Single quotes also stop ${...} substitution.
z:'Price: $5 # not a comment'# Hex colors MUST be quoted (# starts a comment)
z.style.fill:"#FFF4D6"
Nothing to see here: this card is about how text is parsed.
Split diagrams across files. key: @file makes a whole file the value of a key; ...@file spreads its contents into the current map; @file.key takes one object. Leave off the .d2 extension. Paths are relative to the importing file. The Playground cannot do imports, so use the CLI.
main.d2
# Spread a whole file into this map...@styles# Import a file as the value of a key
team:@people# Import just one object from a file
lead:@people.alice
team.bob -> api
api.class: svc
teamAliceapiAliceBob
people.d2
alice: Alice {shape:person}
bob: Bob {shape:person}
styles.d2
classes:{
svc:{style.fill:"#E8EEFF"}}
Shape catalog
Set any of these with shape:. Special shapes with their own rules are covered in the recipes: sql_table, sequence_diagram, plus class for UML (docs), text and image. circle and square keep a 1:1 ratio.
UsersEdgeCore servicesDataCDNLoad balancerAPIAuthWorkerPostgresRedisEvent bus
Layered with orthogonal (right-angle) edges and tidier containers. A good next step when dagre gets tangled, and it points SQL foreign keys at the exact row.
UsersEdgeCore servicesDataCDNLoad balancerAPIAuthWorkerPostgresRedisEvent bus
Built for software architecture: not tied to one hierarchy, favours symmetry, places labels around obstacles. Small edits can reshuffle the whole layout.
Pick an engine with -l elk, D2_LAYOUT=elk, or in the file with vars: {d2-config: {layout-engine: elk}}. List engines and their options with d2 layout and d2 layout tala. For TALA's randomness, --tala-seeds controls the layout attempts.
Feature
dagre
ELK
TALA
direction per container
no
no
yes
near another shape (not just a corner)
no
no
yes
Lock position with top + left
no
no
yes
width / height on containers
no
yes
yes
Connection from a container to its own child
no
yes
yes
SQL foreign keys point at the exact row
no
yes
yes
Routed (not straight) edges between grid cells
no
no
yes
Lever 3: grids
Setting grid-rows and/or grid-columns on the root or on a container switches it to a strict grid, which ignores the engine for placement. Whichever key comes first decides the fill order: rows first fills row by row. Set one key alone and cells stretch to fill. Space them with grid-gap, vertical-gap, horizontal-gap; grid-gap: 0 makes tables and stacks.
near with a corner constant takes a shape out of the flow (titles, legends, notes; see the near card). width and height fix a shape's size. With TALA only, near: other.shape keeps two shapes close and top + left pin one in place while the rest flows around it.
Use for: system context, service maps, deployment views.
Containers for boundaries (cloud, VPC, team), classes to keep styling in one place, a Markdown title with near: top-center, and a legend declared in vars.d2-legend. ELK keeps the edges orthogonal and the container tidy.
Order platform, high levelCustomerAWSStripeAPI gatewayOrders servicePayments serviceEvent busOrders DBSQLOrderPlacedHTTPSchargeLegendInternal serviceThird party
vars:{d2-config:{layout-engine:elk}d2-legend:{
svc: Internal service {style:{fill:"#E8EEFF";stroke:"#0D32B2"}}
ext: Third party {style:{fill:"#F2F4F7";stroke:"#667085";stroke-dash:3}}}}direction:right
title:|md # Order platform, high level|{near:top-center}classes:{
svc:{style:{fill:"#E8EEFF";stroke:"#0D32B2";border-radius:6}}
store:{shape:cylinderstyle:{fill:"#FFF4D6";stroke:"#B7791F"}}
ext:{style:{fill:"#F2F4F7";stroke:"#667085";stroke-dash:3}}}
customer: Customer {shape:person}
aws: AWS {
gw: API gateway {class: svc}
orders: Orders service {class: svc}
pay: Payments service {class: svc}
bus: Event bus {shape:queue}
db: Orders DB {class: store}
gw -> orders
orders -> db: SQL
orders -> bus: OrderPlaced
bus -> pay
}
stripe: Stripe {class: ext}
customer -> aws.gw: HTTPS
aws.pay -> stripe: charge
Use for: API calls, auth handshakes, anything where time order matters.
Set shape: sequence_diagram. Unlike everywhere else, line order is display order, and every actor shares one scope. Declare actors first to fix their order; a nested key on an actor is a span, a container without connections is a group, a nested key with no connections is a note.
shape:sequence_diagram# Declare actors first: this fixes their left-to-right order
user: User {shape:person}
web: Web app
api: API
stripe: Stripe
db: DB
user -> web: Click "Pay"
web -> api: POST /orders
api -> api: validate cart
# A nested key on an actor = a span (activation bar)
api.tx -> db: INSERT order
db -> api.tx: ok
# A container with no connections of its own = a group
payment: Payment {
api -> stripe: charge
stripe -> api: succeeded
}# A nested key with no connections = a note
api."Idempotency key prevents double charges"
api -> web:201 Created
web -> user: Show receipt
Use for: schemas, data models, migrations to review.
Each row of a sql_table is column: type, with an optional constraint (primary_key, foreign_key, unique show as PK, FK, UNQ). Connect rows, not tables, and use ELK or TALA so lines land on the exact row. One recursive glob adds crow's feet everywhere.
vars:{d2-config:{# ELK and TALA point foreign keys at the exact rowlayout-engine:elk}}direction:right
users:{shape:sql_table
id: int {constraint:primary_key}
email: varchar {constraint:unique}
created_at: timestamp
}
orders:{shape:sql_table
id: int {constraint:primary_key}
user_id: int {constraint:foreign_key}
status: varchar
total_cents: int
}
order_items:{shape:sql_table
order_id: int {constraint:[primary_key; foreign_key]}
product_id: int {constraint:[primary_key; foreign_key]}
qty: int
}
products:{shape:sql_table
id: int {constraint:primary_key}
name: varchar
price_cents: int
}# <-> so that both ends can carry an arrowhead
orders.user_id <-> users.id
order_items.order_id <-> orders.id
order_items.product_id <-> products.id
# Crow's feet on every relationship. ** reaches nested rows,# a plain * would only match top-level tables(**<->**)[*]:{source-arrowhead.shape:cf-manytarget-arrowhead.shape:cf-one-required}
Use for: walkthroughs, incident timelines, presentations.
Boards declared under steps each inherit from the previous step. Export them as an animated SVG with d2 --animate-interval 1400 in.d2 out.svg (shown here), as a GIF, or as one slide per step with out.pptx. scenarios inherit from the base instead, and layers start blank.
direction:right
dev: Developer {shape:person}
ci: CI
registry: Registry
cluster: Cluster
# Each step inherits everything from the step before itsteps:{1:{
dev -> ci: git push
}2:{
ci -> registry:image}3:{
registry -> cluster: deploy
cluster.style.fill:"#DCFCE7"}}
Use for: big systems where each audience needs a different slice.
Define everything once, hide it all with suspend globs, then unsuspend what a view needs. Put the model in its own file and import it into each view file. This is more advanced; come back to it once the basics feel natural.
Keys are case-insensitive. Change the label, not the key's case. Docs
A sequence group can't find an actor
In sequence diagrams, actors referenced in a group must be declared at the top level first. Docs
direction on a container does nothing
Per-container direction works only with TALA. Other engines use the root direction. Docs
Container-to-child edge fails
Dagre can't connect a container to its own descendant. Switch to ELK or TALA. Docs
The TALA layout changed completely
TALA has randomness, so small edits can reshuffle it. Pin key shapes with top + left or tune --tala-seeds. Docs
A repeated connection didn't update the first
Repeating a -> b creates a new edge. Edit an existing one with (a -> b)[0].style…. Docs
CLI and config
Command
What it does
d2 -w in.d2 out.svg
Watch mode: render and live-reload in the browser on every save.
d2 in.d2 out.png
Render once. Format from extension: svg, png, pdf, pptx, gif, txt (ASCII).
d2 fmt in.d2
Auto-format in place (indentation, import paths).
d2 validate in.d2
Check syntax without rendering. Useful in CI or before committing.
d2 play in.d2
Open the file in the online Playground, for sharing.
d2 themes
List theme IDs (0 is the default; 200 is a dark theme).
d2 layout [engine]
List engines, or one engine's options.
Flag
Env variable
In d2-config
Effect
-l, --layout
D2_LAYOUT
layout-engine
dagre, elk or tala
-t, --theme
D2_THEME
theme-id
Color theme
--dark-theme
D2_DARK_THEME
dark-theme-id
Theme used when the viewer is in dark mode (SVG)
-s, --sketch
D2_SKETCH
sketch
Hand-drawn look
--pad
D2_PAD
pad
Padding around the diagram (default 100)
-c, --center
D2_CENTER
center
Center the SVG in its viewbox
--animate-interval
D2_ANIMATE_INTERVAL
Pack boards into one animated SVG or GIF (ms)
--target
Render one board, e.g. --target='layers.x'
--scale
SCALE
1 for natural size; default fits SVG to screen
Precedence: flags and environment variables beat d2-config. Putting the config in the file is still the best default, because the diagram then renders the same for everyone. Full list: CLI manual or man d2.
Where to go next
Once this page has you started, pick the path that matches what you need.
I want to…
Look into
Docs
Make every diagram share one look
Classes in a shared file plus imports; globs to change defaults; themes
D2 is younger than Mermaid or PlantUML, so models sometimes mix in syntax from those, or features from older versions. Naming the version and the engine up front avoids most of that, and running d2 validate on the answer catches the rest.
prompt starter
I'm writing a D2 diagram (D2 v0.9, rendered with the CLI).
Layout engine: [dagre | elk | tala]
Diagram type: [architecture | flowchart | sequence | ERD | grid]
What it should show:
[describe the parts and how they connect]
Constraints:
- Short keys, human-readable labels; connections use keys.
- Put styling in `classes`, not on each shape.
- Quote hex colors; put config in vars.d2-config.
- Only use features supported by my layout engine.
Return the complete .d2 file, then list any keyword
you used that isn't basic syntax and what it does.
Written October 2026 against D2 0.9.0 (released September 7, 2026). Every snippet on this page was compiled with that version. Note the project moved from terrastruct/d2 to d2lang/d2; older articles link to the previous home.