CSS styling
Style diagrams with a CSS subset — selectors, cascade, custom properties.
Stylesheets restyle any diagram without touching its code. They work for architecture diagrams and behavior diagrams alike, and render the same in SVG, PowerPoint and Figma.
/* A brand stylesheet: theme tokens, classes, kinds, edges and pseudo-elements. */
:root { --accent: #7C3AED; --danger: #DC2626; }
node { border-radius: 10px; }
node::title { color: var(--accent); }
node.critical { fill: #FEE2E2; stroke: var(--danger); stroke-width: 4; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25); }
node.critical::title { color: #7F1D1D; }
node.external { fill: #F5F3FF; stroke-dasharray: 8 5; }
node.external::title { font-style: italic; }
group.trusted { fill: #ECFDF5; stroke: #059669; stroke-dasharray: 3 3; }
group.trusted::label { color: #065F46; }
edge { stroke: #475569; }
edge[to=ledger] { stroke: #94A3B8; marker-end: circle; }
/* same specificity as the rule above: the later rule wins for the async audit event */
edge.async { stroke: #F59E0B; stroke-dasharray: 6 4; marker-end: open-arrow; }
edge.async::label { color: #92400E; }
"""CSS-like styling: `spinagram render examples/styling/styled.py -f svg,pptx -o out/`."""
from spinagram import Diagram
d = Diagram("C01", "Payments, styled with CSS", subtitle="brand.css: classes, kinds, edges, tokens",
layout={"engine": "layered", "direction": "right"})
d.stylesheet("brand.css")
client = d.box("Partner API", text="REST", class_="external")
with d.group("Trusted zone", class_="trusted"):
gateway = d.box("Gateway", text="NGINX")
payments = d.box("Payments", text="Card capture", class_="critical")
ledger = d.add("data", "Ledger", id="ledger")
audit = d.add("data", "Audit log", id="audit")
client >> gateway >> payments
payments.to(ledger, "write")
payments.to(audit, "event", class_="async")
diagram = d
Attaching styles
| Where | Python | YAML |
|---|---|---|
| Theme | theme.css next to theme.yaml (base themes first) | same |
| Folder | — | stylesheets: [team.css] in _defaults.yaml |
| Diagram | Diagram(css="..."), d.stylesheet("brand.css") | css:, stylesheets: |
| Element | class_="critical", css="stroke-width: 6" | class:, css: |
Later sources win at equal specificity; inline css= beats every normal stylesheet rule and
!important beats inline. An existing style="dark" keeps working and also matches .dark.
Selectors
| Selector | Matches |
|---|---|
node, edge, group, region, lane, diagram | elements by type |
state, decision, action, data, … | nodes of that kind |
.critical, #api, * | class, id, anything |
[icon], [kind=decision], edge[from=api], edge[to=db], [label="yes"] | attributes |
group.vpc node, region > state | descendant, child |
:first-child, :last-child, :not(.x), :root | pseudo-classes |
node::title, node::body, edge::label, group::label | text parts |
legend, step, callout.warning, note.card | annotations |
Properties
| Applies to | Properties |
|---|---|
| Nodes, groups | fill, fill-opacity, stroke, stroke-width, stroke-dasharray, border-radius, opacity, box-shadow |
Text (::title, ::body, ::label) | color, font-size, font-weight, font-style (inherited from groups) |
| Edges | stroke, stroke-width, stroke-dasharray, opacity, marker-start, marker-end (none, arrow, open-arrow, diamond, open-diamond, circle, bar) |
| Legend | placement, width, columns, gap, padding, swatch-width, swatch-height, display (details) |
Colours are hex, rgb()/rgba(), CSS names, theme tokens (fill: primary-soft) or
var(--name), which falls back to the theme token name. Where a format cannot draw a value
(PowerPoint has no open diamond), the nearest equivalent is used and a warning names it.
spinagram check reports stylesheet errors with file and line, unknown properties with a
suggestion, and fonts the theme does not embed.
CSS by backend shows what SVG, PowerPoint and Figma do with each property,
with a rendered example per feature. To see why an element looks the way it does, run
spinagram check --styles or press Inspect in spinagram serve; saving a stylesheet
re-renders the diagrams that use it.