Style that controls its visual presentation —
fills, stroke, label typography, opacity, etc. The Theme block on a
Document provides defaults; per-part style: overrides them.
The four style sub-blocks
palette
Iso parts have three visible faces (top, left, right):
top and left/right cover the lit and shadowed sides
respectively.
Each face can also take a linear gradient instead of a solid
fill — the gradient wins when both are set:
dir is the gradient direction across the face (down is the
common choice — lit top edge falling to a darker base). On
cornerRadius parts, the rounded silhouette has one continuous side
band: it runs rightGradient.from at the top to leftGradient.to
at the bottom.
stroke
width: 0 to hide the silhouette entirely (useful for soft cards).
text
Controls the label rendered on the top face (or screen-space, depending
on orient):
size is an upper bound; content that already fits
keeps its existing placement. When a long word must break, wrapping and
last-line balancing preserve whole grapheme clusters, including combining
accents and emoji sequences. Balancing never moves letters across an
original word boundary.
For system fonts, width estimates are not measurements of the selected font. Fixed
face dimensions can still be too small at the minimum font size: an
oversized cluster remains intact and may overflow. Increase the face size
or use a screen-space caption when the text needs more room. Explicit icon
anchors and offsets retain the author’s placement instead of auto-fitting.
Validation reports a warning when the final estimated content bounds exceed
the preferred top-face padding budget. This may indicate lost breathing room
even when the content is still inside the physical face. Render reports expose
the final lines, font sizes, image viewports and affected sides as content_fits.
An extremely small face retains a positive icon viewport inside the face when
the padding itself is infeasible; that fallback still reports the unmet budget.
An icon never grows merely because the label needs more space.
For reproducible top-face or screen-label text, opt into the bundled Source Sans Pro font:
400/normal, 600, and 700/bold
are supported; use the exact family name rather than a CSS fallback list.
Latin, Greek, Cyrillic and supported punctuation/combining marks use shaped
advances and ink bounds. Missing glyphs, other scripts and system-font
fallback remain explicitly estimated in content_fits; this does not bind
a CJK or emoji font. Sphere labels, free isometric text, connector labels and
the separate top projection retain their existing measurement paths.
For automatically positioned content whose entire label has bound metrics,
the renderer tests rows and wrapped columns using actual glyph heights and
ink gaps. It preserves an already feasible full-size layout; otherwise it
prefers a larger feasible font, then a larger icon, then fewer layout items.
The search has a bounded candidate budget and does not claim a globally
optimal layout. Existing minimum-font and icon-reduction policies remain in
force, with two-decimal SVG placement rounding. Explicit blank lines remain.
Manual icon placement and unsupported-font labels retain their existing
layout policy. When no tested layout satisfies the budget, the full text is
retained and the unmet constraints are reported instead of hiding content.
For bound text, content_fits[].fit.gaps also reports insufficient ink
separation: adjacent visible lines prefer 0.12 em and icon/text boxes prefer
4 local units along either axis. This can produce a warning even when no
outer padding is exceeded. The icon box is its viewport; the measurement
does not certify opaque pixels or readability after shrinking the diagram.
With orient: screen, a label fully supported by the bound font uses its
shaped widths and heights to size the chip, pointer hit rectangle and obstacle
reservation together. Explicit newlines, blank lines and spaces are preserved;
text remains at the requested size, rounded to two decimals for SVG. The chip
keeps at least 8 horizontal and 5 vertical units around the ink, with extra
height for tall marks or multiple lines. Bound-font chips also avoid their
own projected body. Unknown-font or mixed fallback labels retain the existing
estimated single-line layout; they are not certified as shaped blocks.
Screen labels and annotation boxes first try the existing preferred and
peripheral positions. If all fixed candidates collide, a bounded interval
search along four outward rays finds a free position among finite obstacles.
It uses the nearest such position to the preferred location, retaining the
existing direction order for ties, and expands the export bounds as needed.
Dense scenes can therefore place a label farther away; compact global label
assignment and target-scale readability remain separate concerns.
SVG text stays selectable and editable. A composite embeds each used font
weight once; the full TTF resource adds about 330–340 KB per weight to the SVG.
No network font request is needed. Built-in Rasterize/snapshot loads the
same resource into resvg, or uses Chrome/Chromium when resvg is unavailable.
ImageMagick delegates and external/custom rasterizers must be independently
configured for embedded fonts; an explicit ISOTOPO_RASTERIZER keeps its
existing two-argument contract. Default font selection is unchanged.
orient: iso paints the label on the iso-tilted top face. orient: screen paints a horizontal label box BELOW the part’s projected
bounding box — useful for parts that need a readable caption even
when the iso angle would make on-face text hard to read.
effects
dropShadow— Gaussian-blurred offset shadow under the part (screen units; positive dy = down). Use to lift heroes off the canvas.backglow— a blurred halo behind the silhouette. ONE per scene: it marks “the” central element.pattern—hatchordotstexture overlaid on box top faces, cylinder top ellipses, and circle discs.wireframe: true— line-art mode: strokes only, no fills. Pair with a dashed stroke for ghost volumes and floating frames; wireframe/none-filled parts are exempt from overlap warnings.grain: {intensity: 0.45, scale: 0.8}— monochrome film-grain noise on the faces (the “print / risograph” texture). Intensity 0..1; ~0.4–0.5 keeps dark fills dark. Box, rounded box, cylinder.
The Theme block
ATheme is a Style plus optional per-shape-type overrides:
Three-layer cascade
For any part, the renderer computes its final style by merging three sources in order. Later layers override earlier ones; a missing field inherits from the layer above:palette.top: "#FFF" and the part says only palette.left: "#888",
the final part keeps the white top and gains the gray left.
Worked example
db:
- top:
#FCD34D(from part.style — wins) - left:
#0D32B2(from theme.shapes.cylinder — wins over theme) - right:
#1641C5(from theme.shapes.cylinder)
Stroke and effects cascade the same way
Every sub-block follows the same rule. You can layer a theme that sets onlystroke.color with a per-shape default that sets only
effects.cornerRadius with a per-part override of palette.top —
all four sub-blocks merge independently.
When you don’t set a theme
Skiptheme: entirely and parts use built-in defaults (white top,
gray sides, dark stroke, Inter font). This is fine for prototyping;
introduce a theme block when you want brand-consistent output.
When you want NO style at all
Setstyle: null on a part to start from raw defaults, ignoring the
theme. Rarely needed — usually you want some inheritance.
See also
- DSL_YAML.md — where
styleblocks slot into a Document - DSL_D2.md — how d2 styles translate (d2 has its own theme system; we map a subset of its fill/stroke into our cascade)
isotopo capabilities | jq '.style_keys'— programmatic source of truth for every field name listed above