PlantUML theme packs — building, distributing, and enforcing brand themes

puml.online

PlantUML ships with 20+ themes, but they don’t cover corporate brand colour, dark mode, or print colour. This post walks through “DIY theme + internal publish” in full.

Three flavours of a theme

PlantUML themes live at 4 levels:

Level Who uses it File format
skinparam Single diagram / single .puml Inline
!theme file Single team / many diagrams .puml file
Built-in skin pack PlantUML itself Built-in
CSS / SVG transform Post-processing pipeline

The first three have been supported since PlantUML 1.2018. CSS processing is “transform after render” pipeline.

Approach 1: in-line skinparam (smallest)

Put the skin at the top of every diagram:

1
2
3
4
5
6
7
8
9
10
@startuml
skinparam backgroundColor #FAF9F6
skinparam defaultFontName "Inter"
skinparam shadowing false
skinparam nodesep 50
skinparam ArrowColor #5B7C99
skinparam ArrowThickness 1

Alice -> Bob
@enduml

Repeated per diagram — verbose, not great for “team of 30 engineers”.

Improvement: a shared skins/standard.puml:

1
2
3
4
5
6
7
!global
skinparam backgroundColor #FAF9F6
skinparam defaultFontName "Inter"
skinparam shadowing false
skinparam nodesep 50
skinparam ArrowColor #5B7C99
skinparam ArrowThickness 1

Each diagram prepends:

1
!include skins/standard.puml

Less repetitive than per-diagram but !include paths differ across IDEs (VS Code plantuml.includepaths / CLI -I); team collaboration still hurts.

Approach 2: real !theme packs

The !theme syntax from PlantUML 1.2018+ is the “centralised theme resource” story.

Layout

1
2
3
4
5
themes/
├── company-brand/
│ ├── puml-theme.css # colour variables
│ ├── puml-theme.puml # skinparam aggregation
│ └── README.md

puml-theme.css (CSS variables)

1
2
3
4
5
6
7
8
/* Brand colours: violet + warm grey */
--brand-primary: #6B5ACD;
--brand-secondary: #5B7C99;
--brand-bg: #FAF9F6;
--brand-text: #1F2937;

/* Fonts */
--font-sans: "Inter", "Noto Sans CJK SC";

puml-theme.puml (PlantUML syntax)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
!$brand_primary   = "#6B5ACD"
!$brand_secondary = "#5B7C99"
!$brand_bg = "#FAF9F6"
!$brand_text = "#1F2937"
!$font_sans = "Inter, Noto Sans CJK SC"

skinparam backgroundColor $brand_bg
skinparam defaultFontName $font_sans
skinparam ArrowColor $brand_secondary
skinparam ArrowThickness 1
skinparam shadowing false

skinparam activity {
BackgroundColor $brand_primary
BorderColor $brand_secondary
FontColor white
}

skinparam sequence {
LifeLineBorderColor $brand_secondary
ParticipantBorderColor $brand_primary
ParticipantBackgroundColor $brand_bg
ParticipantFontColor $brand_text
ArrowColor $brand_secondary
}

skinparam class {
BackgroundColor $brand_bg
BorderColor $brand_primary
FontColor $brand_text
AttributeFontColor $brand_text
AttributeBackgroundColor transparent
}

Publish to internal npm

1
2
3
4
cd themes/company-brand
npm init -y
# add README.md
npm publish --registry=https://npm.internal.company.com/

Or simpler: push to an internal git repo:

1
2
git remote add internal git@git.internal.company.com:design/plantuml-theme-company.git
git push internal main

Team use

Each diagram:

1
2
@startuml
!theme company-brand

VS Code PlantUML plugin reads !theme:

  • if plantuml-theme-company-brand npm package is installed, automatic discovery
  • otherwise configure !theme company-brand from <path>

Approach 3: post-render transform (CSS / SVG)

After PlantUML renders the SVG, re-color via SVG transform / CSS — fits “we already use the PlantUML service but want to add a corporate theme”.

sed-based SVG colour replace

1
2
3
4
plantuml -tsvg diagram.puml
sed -i 's|fill="#FFFFFF"|fill="#FAF9F6"|g' diagram.svg
sed -i 's|stroke="#000000"|stroke="#5B7C99"|g' diagram.svg
sed -i 's|font-family="sans-serif"|font-family="Inter, Noto Sans CJK SC"|g' diagram.svg

Brute but effective — drawbacks:

  • PlantUML versions emit SVG with different field names (“stroke” vs “Stroke”).
  • Schema varies by diagram type.

Python transform (more reliable)

1
2
3
4
5
6
7
8
9
10
11
import re

def company_transform(svg):
# Find PlantUML defaults → corporate colours
svg = svg.replace('"#FFFFFF"', '"#FAF9F6"')
svg = svg.replace('"#000000"', '"#1F2937"')
svg = svg.replace('"#A80036"', '"#6B5ACD"') # default red → corporate violet
svg = svg.replace('"#0000A0"', '"#5B7C99"') # default blue → accent
# Fonts
svg = re.sub(r'font-family="([^"]+)"', r'font-family="Inter, Noto Sans CJK SC"', svg)
return svg

Drop into a CI pipeline:

1
2
3
4
5
6
- name: Render PlantUML
run: |
for puml in docs/diagrams/*.puml; do
plantuml -tsvg -o /tmp/svg "$puml"
python scripts/company_transform.py < /tmp/svg/$(basename "$puml" .puml).svg > public/img/diagrams/$(basename "$puml" .puml).svg
done

Approach 4: enforce theme consistency at PR review

Inside the team, only .puml files declare !theme company-brand — CI verification:

1
2
3
4
5
6
7
# Pre-commit / GitHub Action
for puml in $(find docs -name '*.puml'); do
if ! grep -q '^!theme ' "$puml"; then
echo "❌ $puml missing !theme declaration"
exit 1
fi
done

Missing theme → CI fails. Conforming diagrams look like:

1
2
3
4
5
@startuml
!theme company-brand

Alice -> Bob: hi
@enduml

Dark mode / light mode auto-switching

PlantUML default doesn’t switch on system preference, but there are workarounds.

Approach A: CSS prefers-color-scheme media query

1
2
3
4
5
6
7
8
/* In the wiki theme CSS */
.diagram-light { display: block; }
.diagram-dark { display: none; }

@media (prefers-color-scheme: dark) {
.diagram-light { display: none; }
.diagram-dark { display: block; }
}

Build-time generate 2 SVGs (light + dark):

1
2
plantuml -tdefault -tsvg diagram.puml   # light
plantuml -tdark -tsvg diagram.puml # dark

In HTML:

1
2
<img src="diagram.svg" class="diagram-light" />
<img src="diagram-dark.svg" class="diagram-dark" />

Approach B: JavaScript toggle class

1
2
3
4
5
document.querySelectorAll('.plantuml-theme-toggle').forEach(el => {
el.addEventListener('click', () => {
document.body.classList.toggle('dark');
});
});

Theme pack contributes:

1
body.dark .diagram { filter: invert(1) hue-rotate(180deg); }

Brute but effective — invert the SVG globally; colour accuracy suffers a bit but works.

Theme version management

For npm publishing, follow semver:

1
2
3
npm version patch   # minor skin param tweaks
npm version minor # add skinparam block
npm version major # breaking changes

Diagrams !theme company-brand without version pinning → use latest. To pin:

1
!theme company-brand@1.2.3

Advanced capabilities

Custom !function

A theme pack can introduce new keywords:

1
2
3
!function $brand_node($name)
!return "rectangle \"==$name\" <<brand>>"
!endfunction

Team diagrams use it directly:

1
2
3
%brand_node("App1")
%brand_node("App2")
App1 -> App2

Custom !pragma

1
!pragma company_layout

In the theme file:

1
2
3
4
!procedure company_layout
!pragma layout elk
skinparam ranksep 80
!endprocedure

Integrate via jsDelivr / CDN

Publish to a GitHub repo, then jsDelivr pulls it:

1
2
3
4
5
git tag v1.0.0
git push --tags

# jsDelivr auto pulls
# https://cdn.jsdelivr.net/gh/your-org/plantuml-theme-company@1.0.0/dist/puml-theme.puml

PlantUML CLI online include:

1
plantuml -tpng -o /tmp -include "https://cdn.jsdelivr.net/..." diagram.puml

Deployment recommendation

By team size:

Team size Approach Reason
1-3 people Inline skinparam Direct
4-20 !theme file + internal git Maintainable, high reuse
20+ npm package + version pin Easy distribution; CI enforces consistency
Multi-product / multi-team Multiple theme packs product / theme isolation

Recap

  • PlantUML themes have 4 layers from “inline” to “publish a package”.
  • Mutable skinparam alone covers most enterprise needs — don’t jump straight to npm.
  • CJK fonts always set skinparam defaultFontName.
  • Team consistency → CI verification → reject diagrams without a theme.

Next

  • Title: PlantUML theme packs — building, distributing, and enforcing brand themes
  • Author: puml.online
  • Created at : 2026-07-30 11:09:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-theme-pack-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.